Repository navigation
feat(scripts): endpoint admin, call limits, background exports (#2220) - #2222
Merged
Merged
Conversation
Adds GET /api/v1/admin/endpoints and /{name} (internal/admin/endpointapi) with the portal's Admin > Automations > Endpoints tab, max_concurrent_calls on api and graphql connections (internal/connlimit), exclusive "queue" for scripts, background graphql_export and api_export runs (internal/exportasync, calltask), timeout cap reporting (internal/timeoutcap), richer graphql_query upstream failures, and manage_table action=check; six tickets, the whole of epic #2220:
1. #2208 endpoints admin: every binding with its traffic over a day or week (requests by status class, failures, p50/p95, distinct signed-in callers, last request) read from audit_logs, and the requests refused before the handler ran, counted per endpoint, hour and reason (bot_check, rate_limit, caller_rate_limit, body_limit, credential, origin) by an in-memory counter flushed every 10s into script_endpoint_refusals (migration 000207, kept 30 days by the retention loop). The portal lists endpoints, opens one on row click to its chart, refusals, top callers and failed requests, turns it on and off, and binds an automation from the tab.
2. #2209 call limits: a connection's max_concurrent_calls (1 to 32) holds its upstream to that many calls at once across every replica through PostgreSQL advisory locks, one per slot, taken in the outbound transport and released when the response is read; a call over the limit waits within its own deadline and a call whose time runs out while waiting fails naming the limit and the wait. A deployment with no database holds the limit per process. list_connections reports call_limits; upstream_calls_inflight and upstream_slot_wait_seconds are recorded; a value that is not a whole number is refused at save. A script's exclusive is now off, refuse or queue (true reads as refuse, migration 000205): with queue a second run is accepted and stays pending until the open run ends. The portal setting reads "Overlapping runs: Allowed, Refused, Queued".
3. #2210 timeout caps: api_invoke_endpoint, api_export, graphql_query and graphql_export report timeout_requested_seconds, timeout_applied_seconds and timeout_capped_by when the connection's call_timeout or the export max_timeout cut the caller's timeout_seconds, and a timed-out call's error names the cap and who owns it.
4. #2211 graphql_query failures: a non-2xx answer with no errors array reports upstream_status, upstream_body (2 KiB, rune-safe), upstream_retryable and retry_after_seconds; a send the endpoint never answered reports transport_error with the endpoint's address redacted and error.code upstream_unavailable. graphql_export reports both the same way.
5. #2212 background exports: graphql_export and api_export take async; a page walk that runs in the background answers at once with a run id and an idempotency_key, records progress with its heartbeat, and calling the export again with the key answers with the result, so a client without the Tasks extension reads it the same way. call_tasks gains idempotency_key, background and progress (migration 000206), one working run per caller, tool and key. platform_info's background_exports names those that ended since the caller was last briefed.
6. #2213 register check: platform.export(register=...) checks the scratch target through manage_table action=check before writing the file, so a refused connection or table name fails before anything is stored; validate reports the same refusal statically, and a refusal names the connections a table can be registered on.
Fixed with it: a deadline reached during the slot's lock query reported a generic failure rather than the call's time running out; a register failure after the write now lists the written output on the run.
Also: regcatalog, handlertransport and timeoutcap move out of their packages for the size budget; ExclusiveMode lives in internal/runstate for pkg/script's surface budget; the import ratchet allowlist gains the new edges; the route gate excuses the endpoint admin routes as it does the bot check settings; docs/server/graphql-gateway.md, api-gateway.md, configuration.md, tools.md, observability.md, docs/scripts/running.md, endpoints.md, the writing-managed-scripts built-in page, CLAUDE.md, llms.txt and llms-full.txt.
Closes #2208
Closes #2209
Closes #2210
Closes #2211
Closes #2212
Closes #2213
Closes #2220
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #2222 +/- ##
==========================================
- Coverage 94.82% 94.79% -0.03%
==========================================
Files 1361 1374 +13
Lines 110410 111373 +963
==========================================
+ Hits 104693 105574 +881
- Misses 5699 5781 +82
Partials 18 18 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds
GET /api/v1/admin/endpointsandGET /api/v1/admin/endpoints/{name}(internal/admin/endpointapi) with the portal's Admin > Automations > Endpoints tab,max_concurrent_callson api and graphql connections (internal/connlimit),exclusive: queuefor managed scripts, backgroundgraphql_exportandapi_exportruns (internal/exportasync,internal/platform/calltask), timeout cap reporting (internal/timeoutcap), structured upstream failures ongraphql_query, andmanage_table action=check. It covers six tickets, the whole of epic #2220. Two defects found along the way are fixed with it (listed after the tickets).GET /api/v1/admin/endpoints?range=day|weeklists every binding ordered by name: its script (id, name, owner), address, methods, path, access (publicorsigned_in),open_reads, whether it is on, its limits, and who last changed it.GET /api/v1/admin/endpoints/{name}adds a series bucketed over the range, the top signed-in callers and the failed requests of the last 7 days.audit_logsrow.bot_check,rate_limit,caller_rate_limit,body_limit,credential,origin. A replica counts in memory and adds its counts every 10s, one row per endpoint, hour and reason, intoscript_endpoint_refusals(migration 000207). A flood moves a counter instead of writing a row per request. The retention loop keeps 30 days (sweependpoint_refusals)./portal/admin/automations/endpoints). A row opens the endpoint's page: a stacked chart of answered requests by status class beside the refused ones, the refusals by reason, the top callers (signed-in endpoints), the failed requests, Turn on/off, and Edit (the automation's page). "Bind an automation" binds one from the tab.max_concurrent_calls(1 to 32) on an api or graphql connection holds its upstream to that many calls at once. A value that is not a whole number is refused at save naming the key; before, a string was stored and read as no limit.upstreamauth.NewHTTPClient) and given back when the response body is read or closed, so an interactive call, an export's page walk and a script'splatform.callall count.list_connectionsanswers a top-levelcall_limitslist (its connection entries are closed objects in older releases, so the field is not added to them). Metricsupstream_calls_inflight{kind,connection}andupstream_slot_wait_seconds{kind,connection}. The connection forms in Admin > Connections carry the field for both kinds.exclusiveis nowoff,refuseorqueue(exclusive_mode, migration 000205, backfilled from the boolean).truestill reads asrefuse. Withqueue, a second run is accepted and stays pending until the open run ends; a run'squeuedflag and a partial unique index hold one queued script to one running run. The script page's setting reads "Overlapping runs: Allowed, Refused, Queued".api_invoke_endpoint,api_export,graphql_queryandgraphql_exportreporttimeout_requested_seconds,timeout_applied_secondsandtimeout_capped_by(connection call_timeoutorexport max_timeout) whenever the caller'stimeout_secondswas cut.errorsarray: a non-2xx answer reportsupstream_status,upstream_body(the first 2 KiB, cut on a rune boundary),upstream_retryableandretry_after_seconds, through the sameupstreamretryreading the api gateway uses. A query is read as a GET and a mutation as a POST, so a 503 is retryable for a query only.transport_error(naming the endpoint with any user info and query string removed),upstream_error: trueanderror.code: upstream_unavailable, where it used to be a bare error result.graphql_exportreports both the same way, and writes nothing.graphql_exportandapi_exporttakeasync. A walk that runs in the background answers at once withstatus: running, arun_id, anidempotency_key(the caller's, or one minted) and the call that reads it again.idempotency_keyanswers with the run's progress while it works and with the export's result once it ends. A client without the Tasks extension reads it the same way, and two calls under one key share one walk.call_tasksgainsidempotency_key,backgroundandprogress(migration 000206), with a unique index holding one working run per caller, tool and key. Progress (pages and items) is written with the heartbeat the call-task runner already stamps.platform_info'sbackground_exportsnames the exports that ended since the caller was last briefed, each with its key.platform.export(..., register=...)asksmanage_table action=checkwhether the connection and table name would be accepted before the file is written, so a refused target fails with "nothing was written". A failure after the write lists the written output on the run.validatereports the same refusal for a literal connection.manage_table action=checkis a read: it answers the catalog, schema and query table a registration would use, writing nothing.Defects fixed with it.
internal/connlimit/connlimit.go,TestShared_DeadlineDuringTheQuery).ui/src/pages/scripts/AdminScriptRoutes.tsx).Also.
internal/platform/tableregister/regcatalog, the in-process transport tointernal/handlertransport, and the cap arithmetic tointernal/timeoutcap;ExclusiveModelives ininternal/runstatesopkg/script's exported surface stays within budget.test/structure/testdata/allowed_internal_imports.txtgains 34 edges and drops one (internal/platform/tableregister -> pkg/catalog, now regcatalog's). The new ones are the six packages above reaching their dependencies,pkg/platforminstalling the limiter and the background runner, and both gateway toolkits reachingexportasync,timeoutcapand (graphql)upstreamretry.TestEveryRouteReportsItsTemplate_RealDBexcuses the two endpoint admin routes as it already does the bot check settings: both are mounted byendpointwire.Mountonly once the script layer has started, which that test does not run.docs/server/graphql-gateway.md,docs/server/api-gateway.md,docs/server/configuration.md,docs/server/tools.md,docs/server/observability.md,docs/scripts/running.md,docs/scripts/endpoints.md, thewriting-managed-scriptsbuilt-in page,CLAUDE.md,docs/llms.txtanddocs/llms-full.txt.Type of Change
Related Issues
Closes #2208
Closes #2209
Closes #2210
Closes #2211
Closes #2212
Closes #2213
Closes #2220
Readers of this state
GET /api/v1/admin/endpointsand/{name}(new;endpointapi.List,Detail,Endpoint,Traffic,Bucket,Caller,Script,StatusClasses).ui/src/pages/scripts/endpoints/EndpointsTab.tsx) and the endpoint page (EndpointDetailPage.tsx) read every field; the types areui/src/api/admin/typesand the hooksui/src/api/admin/hooks.audit_logsandscript_endpoint_refusals, both shared; a replica's refusals reach the table within its 10s flush.ui/src/mocks/data/endpoints.ts,handlers/endpoints.ts) and the screenshot routesadmin-endpointsandadmin-endpoint-detailcarry the shape.exclusiveis now a string (off,refuse,queue) where it was a boolean.PUT /api/v1/portal/scripts/{id}/exclusivetakes the string or a boolean, and the script reads back as the string fromGET /api/v1/portal/scripts/{id},manage_script getandlist.ui/src/pages/scripts/ScriptExclusiveSetting.tsx) reads and writes the three modes.run_scriptread it at run creation; a queued run is held by the claim on every replica.test/acceptance/issue_1986_test.goreads back the mode names.list_connectionsgains top-levelcall_limits. Read by the acceptance test; the connection entries are unchanged.max_concurrent_calls. The api and graphql connection forms (ApiGatewayConfigForm,GraphQLConfigForm) read and write it;GET /api/v1/admin/connection-instances/{kind}/{name}returns it as stored.platform_infogains top-levelbackground_exports. Read by the acceptance test and by an agent at session start.timeout_*on the four gateway tools;upstream_body,upstream_retryable,retry_after_seconds,transport_erroranderrorongraphql_queryandgraphql_export;status,run_id,idempotency_keyandprogresson a background export. Every output schema is open to the platform's keys (MCPOutputSchemaMiddleware), andtools/listcarries the newasyncinput.Testing
Acceptance through the real surface on the local dev stack (two replicas behind the proxy), every criterion passing; transcripts in
build/2208throughbuild/2213and thebuild/1986addendum.timeout_seconds1 behind a held slot fails namingmax_concurrent_callsand its wait;list_connectionsnames the limit; a string limit is refused with 400; withexclusive: "queue"a second run stays pending and starts after the first finishes.graphql_querywithcall_timeout5s andtimeout_seconds240 reports the cap;api_invoke_endpointstopped at a 1scall_timeoutnames it.transport_errorandupstream_unavailable.graphql_exportpage walk runs in the background and is read again by its key; anapi_exportstarted withasyncis joined, not walked twice, under its key.manage_table checkanswers on a scratch connection and refuses one without a scratch target, naming the connection that has one; a draft registering on a connection with no scratch target fails with nothing written, andvalidateflags it.Real-Postgres: the advisory-lock limit across two pools, queued-run claims, background-export storage and its unique key, the refusal store and the endpoint traffic aggregates, and the SQL statement gate.
make verifypassed: the full Go race and coverage run (total 93.9%, patch 89.7%), the schedule lane, the real-database lane, the full vitest suite (3,231) and Playwright (296).Ran
make testlocallyTested manually
Added new tests for changes (if applicable)
Checklist
make lint)