Skip to content

Commit 0bcf3ac

Browse files
committed
Merge feat/run-ops-shards-tri-13429 into feat/sentinels-replication-n-tri-13432
2 parents d840176 + c1a5d70 commit 0bcf3ac

93 files changed

Lines changed: 6640 additions & 895 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/local-bundle-deploy.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@trigger.dev/core": patch
3+
"trigger.dev": patch
4+
---
5+
6+
Add an experimental `--local-bundle` deploy flag that runs the install and bundling steps on your machine and uploads only the build output; the image is still built remotely. Useful when your project's install step needs tooling or credentials that only exist locally.

.github/workflows/codeql.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: CodeQL
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
permissions: {}
9+
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
analyze:
16+
name: Analyze (${{ matrix.language }})
17+
if: github.repository == 'triggerdotdev/trigger.dev'
18+
runs-on: ubuntu-latest
19+
permissions:
20+
contents: read
21+
security-events: write # Upload SARIF to GitHub Security tab
22+
strategy:
23+
fail-fast: false
24+
matrix:
25+
language: [actions, javascript-typescript]
26+
steps:
27+
- name: Checkout repository
28+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
29+
with:
30+
persist-credentials: false
31+
32+
- name: Initialize CodeQL
33+
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
34+
with:
35+
languages: ${{ matrix.language }}
36+
37+
- name: Perform CodeQL Analysis
38+
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
39+
with:
40+
category: /language:${{ matrix.language }}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Self-hosted instances can now disable the admin dashboard and user impersonation entirely. See the self-hosting docs for the new setting.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Deployment logs no longer jump to the bottom while you are reading earlier output. Scroll up to pause auto-scroll, and scroll back down or use the new scroll-to-bottom button in the log header to resume following.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
When a runs list or runs.list API request spans too much data to complete, it now returns a clear, actionable error asking you to narrow the time range, instead of failing with a generic error.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Improved the performance and reliability of the runs list and the runs.list API, especially for large projects and filtered views.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Callout } from "~/components/primitives/Callout";
2+
3+
/**
4+
* Error state for a runs list that failed to load. Shown as the `errorElement` of the deferred
5+
* runs-list data. The most common recoverable cause is a query that was too expensive over a broad
6+
* time range (see `RunsListQueryError`), so the copy guides narrowing the range; a refresh covers
7+
* transient failures. The precise reason is not shown because Remix scrubs thrown error messages in
8+
* production.
9+
*/
10+
export function RunsListErrorState() {
11+
return (
12+
<div className="flex items-center justify-center px-3 py-12">
13+
<Callout variant="error" className="max-w-fit">
14+
We couldn't load these runs. If you're filtering over a broad time range, try narrowing it,
15+
then refresh to try again.
16+
</Callout>
17+
</div>
18+
);
19+
}
20+
21+
/**
22+
* Renders nothing. Used as the `errorElement` for secondary awaits of the same runs-list promise
23+
* (e.g. the pagination controls), so a rejection is handled locally there and does not bubble to
24+
* the route error boundary. The primary awaits render {@link RunsListErrorState}.
25+
*/
26+
export function RunsListErrorStateNoop() {
27+
return null;
28+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { describe, expect, it } from "vitest";
2+
import { DeploymentLogsCache, type DeploymentLogEntry } from "./deploymentLogsCache";
3+
4+
function lines(count: number): DeploymentLogEntry[] {
5+
return Array.from({ length: count }, (_, i) => ({
6+
message: `line ${i}`,
7+
timestamp: new Date(0),
8+
level: "info" as const,
9+
}));
10+
}
11+
12+
describe("DeploymentLogsCache", () => {
13+
it("returns undefined for unknown keys", () => {
14+
const cache = new DeploymentLogsCache(2, 100);
15+
expect(cache.get("missing")).toBeUndefined();
16+
});
17+
18+
it("stores and returns entries", () => {
19+
const cache = new DeploymentLogsCache(2, 100);
20+
const value = { logs: lines(3), nextSeqNum: 3, finalized: true, complete: true };
21+
cache.set("a", value);
22+
expect(cache.get("a")).toBe(value);
23+
expect(cache.size).toBe(1);
24+
expect(cache.lineCount).toBe(3);
25+
});
26+
27+
it("evicts the least recently used deployment past the entry limit", () => {
28+
const cache = new DeploymentLogsCache(2, 100);
29+
cache.set("a", { logs: lines(1), nextSeqNum: 1, finalized: false, complete: false });
30+
cache.set("b", { logs: lines(1), nextSeqNum: 1, finalized: false, complete: false });
31+
cache.get("a");
32+
cache.set("c", { logs: lines(1), nextSeqNum: 1, finalized: false, complete: false });
33+
34+
expect(cache.get("b")).toBeUndefined();
35+
expect(cache.get("a")).toBeDefined();
36+
expect(cache.get("c")).toBeDefined();
37+
expect(cache.size).toBe(2);
38+
});
39+
40+
it("evicts oldest deployments past the total line budget", () => {
41+
const cache = new DeploymentLogsCache(10, 10);
42+
cache.set("a", { logs: lines(4), nextSeqNum: 4, finalized: true, complete: true });
43+
cache.set("b", { logs: lines(4), nextSeqNum: 4, finalized: true, complete: true });
44+
cache.set("c", { logs: lines(4), nextSeqNum: 4, finalized: true, complete: true });
45+
46+
expect(cache.get("a")).toBeUndefined();
47+
expect(cache.get("b")).toBeDefined();
48+
expect(cache.get("c")).toBeDefined();
49+
expect(cache.lineCount).toBe(8);
50+
});
51+
52+
it("always keeps the entry just set, even when it alone exceeds the budget", () => {
53+
const cache = new DeploymentLogsCache(10, 10);
54+
cache.set("a", { logs: lines(4), nextSeqNum: 4, finalized: true, complete: true });
55+
cache.set("big", { logs: lines(50), nextSeqNum: 50, finalized: true, complete: true });
56+
57+
expect(cache.get("a")).toBeUndefined();
58+
expect(cache.get("big")?.logs).toHaveLength(50);
59+
expect(cache.size).toBe(1);
60+
expect(cache.lineCount).toBe(50);
61+
});
62+
63+
it("treats replacing a key as a recent use", () => {
64+
const cache = new DeploymentLogsCache(2, 100);
65+
cache.set("a", { logs: lines(1), nextSeqNum: 1, finalized: false, complete: false });
66+
cache.set("b", { logs: lines(1), nextSeqNum: 1, finalized: false, complete: false });
67+
cache.set("a", { logs: lines(2), nextSeqNum: 2, finalized: true, complete: true });
68+
cache.set("c", { logs: lines(1), nextSeqNum: 1, finalized: false, complete: false });
69+
70+
expect(cache.get("b")).toBeUndefined();
71+
expect(cache.get("a")?.logs).toHaveLength(2);
72+
expect(cache.get("c")).toBeDefined();
73+
});
74+
75+
it("keeps recently read deployments when evicting for the line budget", () => {
76+
const cache = new DeploymentLogsCache(10, 10);
77+
cache.set("a", { logs: lines(4), nextSeqNum: 4, finalized: true, complete: true });
78+
cache.set("b", { logs: lines(4), nextSeqNum: 4, finalized: true, complete: true });
79+
cache.get("a");
80+
cache.set("c", { logs: lines(4), nextSeqNum: 4, finalized: true, complete: true });
81+
82+
expect(cache.get("b")).toBeUndefined();
83+
expect(cache.get("a")).toBeDefined();
84+
expect(cache.get("c")).toBeDefined();
85+
expect(cache.lineCount).toBe(8);
86+
});
87+
88+
it("replaces an existing key without double counting lines", () => {
89+
const cache = new DeploymentLogsCache(10, 100);
90+
cache.set("a", { logs: lines(4), nextSeqNum: 4, finalized: false, complete: false });
91+
cache.set("a", { logs: lines(6), nextSeqNum: 6, finalized: true, complete: true });
92+
93+
expect(cache.size).toBe(1);
94+
expect(cache.lineCount).toBe(6);
95+
expect(cache.get("a")?.complete).toBe(true);
96+
});
97+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
export type DeploymentLogEntry = {
2+
message: string;
3+
timestamp: Date;
4+
level: "info" | "error" | "warn" | "debug";
5+
};
6+
7+
export type CachedDeploymentLogs = {
8+
logs: readonly DeploymentLogEntry[];
9+
nextSeqNum: number;
10+
finalized: boolean;
11+
complete: boolean;
12+
};
13+
14+
export class DeploymentLogsCache {
15+
private entries = new Map<string, CachedDeploymentLogs>();
16+
private totalLines = 0;
17+
18+
constructor(
19+
private readonly maxDeployments: number,
20+
private readonly maxTotalLines: number
21+
) {}
22+
23+
get(key: string): CachedDeploymentLogs | undefined {
24+
const entry = this.entries.get(key);
25+
if (!entry) return undefined;
26+
this.entries.delete(key);
27+
this.entries.set(key, entry);
28+
return entry;
29+
}
30+
31+
set(key: string, value: CachedDeploymentLogs) {
32+
const existing = this.entries.get(key);
33+
if (existing) {
34+
this.totalLines -= existing.logs.length;
35+
this.entries.delete(key);
36+
}
37+
this.entries.set(key, value);
38+
this.totalLines += value.logs.length;
39+
40+
for (const [oldestKey, oldest] of this.entries) {
41+
if (oldestKey === key) break;
42+
if (this.entries.size <= this.maxDeployments && this.totalLines <= this.maxTotalLines) break;
43+
this.entries.delete(oldestKey);
44+
this.totalLines -= oldest.logs.length;
45+
}
46+
}
47+
48+
get size() {
49+
return this.entries.size;
50+
}
51+
52+
get lineCount() {
53+
return this.totalLines;
54+
}
55+
}
56+
57+
export const deploymentLogsCache = new DeploymentLogsCache(20, 20_000);

apps/webapp/app/db.server.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,14 +578,19 @@ export const runOpsLegacyReplicaClient: RunOpsPrismaClient = runOpsTopology.lega
578578
.replica as unknown as RunOpsPrismaClient;
579579

580580
// Gen-2 shard handles for the run-store boundary. Empty unless RUN_OPS_SHARDS is configured.
581+
// `aliasOf` carries the descriptor's declared alias so the router can dedup an aliased shard (which
582+
// shares its target's database) out of every fan-out sum — the DECLARATION, not client identity.
583+
const runOpsShardAliasByKey = new Map(env.RUN_OPS_SHARDS.map((d) => [d.key, d.aliasOf]));
581584
export const runOpsShardHandles: Array<{
582585
key: string;
583586
writer: RunOpsPrismaClient;
584587
replica: RunOpsPrismaClient;
588+
aliasOf?: string;
585589
}> = [...runOpsTopology.shards.entries()].map(([key, clients]) => ({
586590
key,
587591
writer: clients.writer,
588592
replica: clients.replica,
593+
aliasOf: runOpsShardAliasByKey.get(key),
589594
}));
590595

591596
export const runOpsSplitReadEnabled: boolean = computeRunOpsSplitReadEnabled({

0 commit comments

Comments
 (0)