Skip to content

Commit fbd5d2f

Browse files
d-csclaude
andcommitted
revert(webapp): drop mollifier listing-merge from runs list
The runs list (API and dashboard) is eventually consistent — buffered runs were creating a sandwich problem where the head of the list could include buffered rows while in-transit rows between PG replication and ClickHouse went missing. Drop the merge so the list returns PG/ ClickHouse rows only; buffered visibility will return via a separate global status indicator. Reverts the merge wiring in api.v1.runs, api.v1.projects.$projectRef .runs, and the dashboard runs index, and deletes listingMerge.server and dashboardListingMerge.server. The MCP list_runs tool rides through the API and inherits the same behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d4b55c1 commit fbd5d2f

6 files changed

Lines changed: 16 additions & 704 deletions

File tree

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+
Runs list (API and dashboard) is eventually consistent: drop the mollifier-buffer merge so buffered runs no longer appear in `apiClient.listRuns` or the dashboard runs index. Buffered visibility will return via a separate global status indicator.

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,6 @@ import { findProjectBySlug } from "~/models/project.server";
4545
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
4646
import { getRunFiltersFromRequest } from "~/presenters/RunFilters.server";
4747
import { NextRunListPresenter } from "~/presenters/v3/NextRunListPresenter.server";
48-
import {
49-
dashboardListCursor,
50-
mergeBufferedIntoDashboardList,
51-
} from "~/v3/mollifier/dashboardListingMerge.server";
5248
import { clickhouseClient } from "~/services/clickhouseInstance.server";
5349
import {
5450
setRootOnlyFilterPreference,
@@ -93,44 +89,13 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
9389

9490
const filters = await getRunFiltersFromRequest(request);
9591

96-
// Buffered-run pagination uses a compound cursor that wraps the PG
97-
// presenter's own cursor. Decode here so the inner PG cursor is
98-
// forwarded to the presenter; the merge helper reconstructs the
99-
// outgoing cursor based on what fits on this page.
100-
const decodedCursor = dashboardListCursor.decode(filters.cursor);
101-
const pgCursor = decodedCursor ? decodedCursor.inner : filters.cursor;
102-
const dashboardPageSize = 25;
103-
10492
const presenter = new NextRunListPresenter($replica, clickhouseClient);
105-
const baseList = presenter.call(project.organizationId, environment.id, {
93+
const list = presenter.call(project.organizationId, environment.id, {
10694
userId,
10795
projectId: project.id,
10896
...filters,
109-
cursor: pgCursor,
11097
});
11198

112-
// Prepend mollifier-buffered runs so customers see freshly-triggered
113-
// runs while the gate is diverting traffic. The merge happens inside
114-
// the deferred promise so the page still streams.
115-
const list = baseList.then((result) =>
116-
mergeBufferedIntoDashboardList({
117-
baseList: result,
118-
envId: environment.id,
119-
pageSize: dashboardPageSize,
120-
cursor: filters.cursor,
121-
filters: {
122-
tasks: filters.tasks,
123-
statuses: filters.statuses,
124-
tags: filters.tags,
125-
period: filters.period,
126-
from: filters.from,
127-
to: filters.to,
128-
isTest: filters.isTest,
129-
runId: filters.runId,
130-
},
131-
})
132-
);
133-
13499
// Only persist rootOnly when no tasks are filtered. While a task filter is active,
135100
// the toggle's URL value can be a temporary auto-flip (or a user override scoped to
136101
// the current task filter), and we don't want either bleeding into the saved

apps/webapp/app/routes/api.v1.projects.$projectRef.runs.ts

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
ApiRunListSearchParams,
88
} from "~/presenters/v3/ApiRunListPresenter.server";
99
import { createLoaderPATApiRoute } from "~/services/routeBuilders/apiBuilder.server";
10-
import { callRunListWithBufferMerge } from "~/v3/mollifier/listingMerge.server";
1110

1211
const ParamsSchema = z.object({
1312
projectRef: z.string(),
@@ -40,35 +39,6 @@ export const loader = createLoaderPATApiRoute(
4039
return json({ error: "Project not found" }, { status: 404 });
4140
}
4241

43-
// For PAT-scoped lookups the environment isn't supplied by auth;
44-
// it's resolved from `filter[env]`. The presenter already does this
45-
// lookup internally and errors if no env can be resolved. We mirror
46-
// that resolution here so the mollifier-buffer merge has the env
47-
// context it needs (envId + slug for synthesised list items).
48-
const envFilter = searchParams["filter[env]"];
49-
let envForMerge:
50-
| { id: string; organizationId: string; slug: string }
51-
| undefined;
52-
if (envFilter && envFilter.length > 0) {
53-
const env = await $replica.runtimeEnvironment.findFirst({
54-
where: { projectId: project.id, slug: { in: envFilter } },
55-
select: { id: true, organizationId: true, slug: true },
56-
});
57-
if (env) envForMerge = env;
58-
}
59-
60-
if (envForMerge) {
61-
const result = await callRunListWithBufferMerge({
62-
project,
63-
searchParams,
64-
apiVersion,
65-
environment: envForMerge,
66-
});
67-
return json(result);
68-
}
69-
70-
// No env resolvable — let the presenter throw its existing
71-
// ServiceValidationError, preserving the legacy behaviour.
7242
const presenter = new ApiRunListPresenter();
7343
const result = await presenter.call(project, searchParams, apiVersion);
7444

apps/webapp/app/routes/api.v1.runs.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { json } from "@remix-run/server-runtime";
2-
import { ApiRunListSearchParams } from "~/presenters/v3/ApiRunListPresenter.server";
3-
import { logger } from "~/services/logger.server";
2+
import {
3+
ApiRunListPresenter,
4+
ApiRunListSearchParams,
5+
} from "~/presenters/v3/ApiRunListPresenter.server";
46
import {
57
anyResource,
68
createLoaderApiRoute,
79
} from "~/services/routeBuilders/apiBuilder.server";
8-
import { callRunListWithBufferMerge } from "~/v3/mollifier/listingMerge.server";
910

1011
export const loader = createLoaderApiRoute(
1112
{
@@ -36,12 +37,13 @@ export const loader = createLoaderApiRoute(
3637
findResource: async () => 1, // This is a dummy function, we don't need to find a resource
3738
},
3839
async ({ searchParams, authentication, apiVersion }) => {
39-
const result = await callRunListWithBufferMerge({
40-
project: authentication.environment.project,
40+
const presenter = new ApiRunListPresenter();
41+
const result = await presenter.call(
42+
authentication.environment.project,
4143
searchParams,
4244
apiVersion,
43-
environment: authentication.environment,
44-
});
45+
authentication.environment
46+
);
4547

4648
return json(result);
4749
}

0 commit comments

Comments
 (0)