-
Notifications
You must be signed in to change notification settings - Fork 40
feat(carve S7): the orchestration compute plane — reconciler, release, rollup #659
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
isadeks
wants to merge
38
commits into
carve/s1-foundation-contracts
Choose a base branch
from
carve/s7-orchestration-plane
base: carve/s1-foundation-contracts
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
52b9b4c
feat(carve S2): ECS rightsized planning task def + configurable task …
isadeks d469bc2
fix(carve S2): de-opinionate the build task — modest default, overrid…
isadeks 75379a9
fix(carve S2): make the sizing knobs actually reachable, and refuse p…
isadeks 57a1ed3
fix(carve S2): give the build task real disk margin — 21 → 50 GiB
isadeks 81155bb
fix(carve S2): make the planning task def reachable in the slice that…
isadeks f9d16ac
docs(carve S2): describe artifact workflows by their contract, not by…
isadeks 3a38156
feat(carve S3): the agent runtime (Linear no-MCP, attachments, build/…
isadeks ce47f5d
test: tolerate either frozenset layout when cross-checking the writea…
isadeks 206a3b6
fix(carve S3): keep the Jira app-identity work when applying the agen…
isadeks 566dc3e
fix(carve S3): review findings — close a clone-guard bypass, grant th…
isadeks 88f27ef
fix(carve S3): the clone-guard rewrite broke two things it was meant …
isadeks 7568743
fix(carve S3): report a signal-killed build as infra, not as broken code
isadeks b34b07f
refactor(carve S3): drop the decomposition workflow from the main-bou…
isadeks 698a6ce
feat(carve S4): Linear issue-context surface — attachments, PDF/image…
isadeks d4e0cb3
fix(carve S4): review findings — two attachment defects, and stop shi…
isadeks 2f98e44
fix(carve S4): bound the scan input, not just the label — the label b…
isadeks 4e165c5
refactor(carve S4): drop the decomposition config fields from the tab…
isadeks fbf6516
refactor(carve S4): drop the CLI flags for a feature main will not have
isadeks 7ca5ef0
fix(carve S4): give pdf-parse its own copy so the caller can still up…
isadeks 55ece46
feat(carve S5): orchestration DAG core, row store, and the channel ab…
isadeks a6913c6
fix(carve S5): keep no-shadow enforced in tests rather than relaxing …
isadeks 8633acf
docs(carve S5): fix comments that describe code this slice does not c…
isadeks 157d3f1
feat(carve S6): auto-decomposition planning and iteration feedback (d…
isadeks 5163733
fix(carve S6): review findings — explain the guardrail exception, fix…
isadeks ac523da
fix(carve S6): the regex fix over-corrected, and the guardrail commen…
isadeks e48273b
refactor(carve S6): reduce this slice to the iteration work, without …
isadeks d9047fa
feat(carve S7): the orchestration compute plane — reconciler, release…
isadeks 8c1e579
fix(carve S7): the retry hint must name the project's own trigger label
isadeks 842f89b
fix(carve S7): actually wire the project's trigger label to the retry…
isadeks 78a6053
fix(carve S7): populate the trigger label instead of always rendering…
isadeks 6a43bdd
refactor(carve S7): remove graph CREATION from the reconciler, keep g…
isadeks 3de6a54
fix(carve S7): show the parent panel a preview even when there is no …
isadeks 19acf10
fix(carve S7): pin the coding workflow when releasing an orchestratio…
isadeks 30e7c13
fix(carve S7): give every child the epic's shared contract, and let a…
isadeks e8aa93c
fix(carve S7): state the shared context descriptively so the guardrai…
isadeks 4b730ad
Merge the merged carve base into the orchestration-plane slice
isadeks 3ac65d7
fix(carve S7): bound and paginate the iteration cost sum, and derive …
isadeks e7ddc01
style(carve S7): order the new test import as the linter does
isadeks File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| /** | ||
| * MIT No Attribution | ||
| * | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * | ||
| * Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
| * the Software without restriction, including without limitation the rights to | ||
| * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | ||
| * the Software, and to permit persons to whom the Software is furnished to do so. | ||
| * | ||
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| * SOFTWARE. | ||
| */ | ||
|
|
||
| import * as path from 'path'; | ||
| import { Duration } from 'aws-cdk-lib'; | ||
| import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; | ||
| import { Architecture, FilterCriteria, FilterRule, Runtime, StartingPosition } from 'aws-cdk-lib/aws-lambda'; | ||
| import { DynamoEventSource, SqsDlq } from 'aws-cdk-lib/aws-lambda-event-sources'; | ||
| import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs'; | ||
| import * as sqs from 'aws-cdk-lib/aws-sqs'; | ||
| import { NagSuppressions } from 'cdk-nag'; | ||
| import { Construct } from 'constructs'; | ||
| import { TERMINAL_STATUSES } from './task-status'; | ||
|
|
||
| /** | ||
| * Properties for OrchestrationReconciler construct. | ||
| */ | ||
| export interface OrchestrationReconcilerProps { | ||
| /** | ||
| * TaskTable — MUST have a stream enabled (NEW_IMAGE). This construct is | ||
| * the table's stream consumer; the reconciler reacts to child tasks | ||
| * reaching terminal status. | ||
| */ | ||
| readonly taskTable: dynamodb.ITable; | ||
|
|
||
| /** OrchestrationTable — the reconciler reads the DAG + writes child statuses. */ | ||
| readonly orchestrationTable: dynamodb.ITable; | ||
|
|
||
| /** Orchestrator function ARN — releaseChild → createTaskCore invokes it. */ | ||
| readonly orchestratorFunctionArn?: string; | ||
|
|
||
| /** Forwarded so released child tasks land in the right tables. */ | ||
| readonly taskEventsTable: dynamodb.ITable; | ||
| } | ||
|
|
||
| /** | ||
| * TaskTable-stream consumer that drives Linear parent/sub-issue | ||
| * orchestration. On each child task reaching a | ||
| * terminal status it releases newly-unblocked children in dependency | ||
| * order (see `handlers/orchestration-reconciler.ts`). | ||
| * | ||
| * Stream-source rationale: TaskEventsTable's stream is at its 2-consumer | ||
| * limit (FanOutConsumer + ApprovalMetricsPublisher); TaskTable had no | ||
| * stream, so the reconciler is its first and only consumer — zero | ||
| * contention with the fan-out plane. | ||
| */ | ||
|
|
||
| /** DLQ message retention (days) — long enough for an operator to inspect a | ||
| * poison stream record before it ages out. */ | ||
| const DLQ_RETENTION_DAYS = 14; | ||
|
|
||
| export class OrchestrationReconciler extends Construct { | ||
| public readonly fn: lambda.NodejsFunction; | ||
| public readonly dlq: sqs.Queue; | ||
|
|
||
| constructor(scope: Construct, id: string, props: OrchestrationReconcilerProps) { | ||
| super(scope, id); | ||
|
|
||
| const handlersDir = path.join(__dirname, '..', 'handlers'); | ||
|
|
||
| this.fn = new lambda.NodejsFunction(this, 'ReconcilerFn', { | ||
| entry: path.join(handlersDir, 'orchestration-reconciler.ts'), | ||
| handler: 'handler', | ||
| runtime: Runtime.NODEJS_24_X, | ||
| architecture: Architecture.ARM_64, | ||
| timeout: Duration.minutes(2), | ||
| // 512 MB (not 256): the reconciler bundles createTaskCore, which | ||
| // pulls in the Bedrock guardrail + S3 attachment-screening SDK | ||
| // stack. At 256 MB it OOMs during init on every stream event | ||
| // (Max Memory Used 255/256 MB) and never releases children. The | ||
| // LinearIntegration webhook processor runs the same code at 512 MB. | ||
| memorySize: 512, | ||
| environment: { | ||
| ORCHESTRATION_TABLE_NAME: props.orchestrationTable.tableName, | ||
| TASK_TABLE_NAME: props.taskTable.tableName, | ||
| TASK_EVENTS_TABLE_NAME: props.taskEventsTable.tableName, | ||
| ...(props.orchestratorFunctionArn && { | ||
| ORCHESTRATOR_FUNCTION_ARN: props.orchestratorFunctionArn, | ||
| }), | ||
| }, | ||
| bundling: { | ||
| externalModules: ['@aws-sdk/*'], | ||
| // pdf-parse (v2, pdfjs-based) can't be esbuild-bundled — its pdfjs/native | ||
| // deps break at import. The reconciler screens the parent issue's PDF | ||
| // attachments, so ship pdf-parse | ||
| // unbundled to resolve natively at runtime. MUST match the webhook | ||
| // processors' attachment-screening bundling — a Lambda that CALLS | ||
| // attachment-screening but omits this carve-out fails every PDF at | ||
| // runtime while passing every unit test — a failure mode observed in | ||
| // practice on the attachment path. | ||
| nodeModules: ['pdf-parse'], | ||
| }, | ||
| }); | ||
|
|
||
| // DLQ for poison stream records (a record that repeatedly fails the | ||
| // reconcile). Fan-out uses the same pattern; without it a bad record | ||
| // would block the shard. | ||
| this.dlq = new sqs.Queue(this, 'ReconcilerDlq', { | ||
| retentionPeriod: Duration.days(DLQ_RETENTION_DAYS), | ||
| enforceSSL: true, | ||
| }); | ||
|
|
||
| // Orchestration child creation/gating reads + writes the DAG table, | ||
| // reads/writes TaskTable (createTaskCore), and writes task events. | ||
| props.orchestrationTable.grantReadWriteData(this.fn); | ||
| props.taskTable.grantReadWriteData(this.fn); | ||
| props.taskEventsTable.grantReadWriteData(this.fn); | ||
|
|
||
| // Subscribe to the TaskTable stream. LATEST: we only care about | ||
| // tasks transitioning to terminal from here on. bisectBatchOnError + | ||
| // DLQ so one poison record can't wedge the shard. | ||
| // | ||
| // FilterCriteria: the handler ignores every non-terminal status | ||
| // (parseTerminalTaskRecord returns null unless status ∈ TERMINAL), so the | ||
| // stream itself filters to terminal statuses. This keeps RUNNING/HYDRATING/ | ||
| // heartbeat/progress writes — the bulk of TaskTable churn platform-wide — | ||
| // from ever invoking this 512MB reconciler. Behavior-preserving: the records | ||
| // dropped here are exactly the ones the handler already discarded. One filter | ||
| // pattern per terminal status (FilterCriteria ORs the array). | ||
| const terminalFilters = TERMINAL_STATUSES.map((s) => FilterCriteria.filter({ | ||
| dynamodb: { NewImage: { status: { S: FilterRule.isEqual(s) } } }, | ||
| })); | ||
| this.fn.addEventSource(new DynamoEventSource(props.taskTable, { | ||
| startingPosition: StartingPosition.LATEST, | ||
| batchSize: 10, | ||
| retryAttempts: 3, | ||
| bisectBatchOnError: true, | ||
| onFailure: new SqsDlq(this.dlq), | ||
| filters: terminalFilters, | ||
| // Partial-batch reporting. Without this, the handler | ||
| // returning void meant ANY thrown record failed the WHOLE batch — a single | ||
| // poison/throttled record re-drove all its siblings (re-reconciling | ||
| // healthy children) until it aged out. With reportBatchItemFailures the | ||
| // handler returns only the failed record's sequence number, so just that | ||
| // record retries + bisects toward the DLQ while its siblings commit. | ||
| reportBatchItemFailures: true, | ||
| })); | ||
|
|
||
| NagSuppressions.addResourceSuppressions(this.fn, [ | ||
| { | ||
| id: 'AwsSolutions-IAM4', | ||
| reason: 'AWSLambdaBasicExecutionRole is required for CloudWatch Logs access', | ||
| }, | ||
| { | ||
| id: 'AwsSolutions-IAM5', | ||
| reason: | ||
| 'DynamoDB index/* + stream ARN wildcards generated by CDK grantReadWriteData ' | ||
| + '(ChildTaskIndex query) and the DynamoEventSource read access', | ||
| }, | ||
| ], true); | ||
|
|
||
| NagSuppressions.addResourceSuppressions(this.dlq, [ | ||
| { | ||
| id: 'AwsSolutions-SQS3', | ||
| reason: | ||
| 'This queue IS the DLQ for the reconciler stream consumer — having its own DLQ would be infinite recursion', | ||
| }, | ||
| ]); | ||
| } | ||
| } |
126 changes: 126 additions & 0 deletions
126
cdk/src/constructs/stranded-orchestration-reconciler.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| /** | ||
| * MIT No Attribution | ||
| * | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * | ||
| * Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
| * the Software without restriction, including without limitation the rights to | ||
| * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | ||
| * the Software, and to permit persons to whom the Software is furnished to do so. | ||
| * | ||
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| * SOFTWARE. | ||
| */ | ||
|
|
||
| import * as path from 'path'; | ||
| import { Duration } from 'aws-cdk-lib'; | ||
| import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; | ||
| import * as events from 'aws-cdk-lib/aws-events'; | ||
| import * as targets from 'aws-cdk-lib/aws-events-targets'; | ||
| import { Architecture, Runtime } from 'aws-cdk-lib/aws-lambda'; | ||
| import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs'; | ||
| import { NagSuppressions } from 'cdk-nag'; | ||
| import { Construct } from 'constructs'; | ||
|
|
||
| /** | ||
| * Properties for StrandedOrchestrationReconciler construct. | ||
| */ | ||
| export interface StrandedOrchestrationReconcilerProps { | ||
| /** OrchestrationTable — read DAG state, write recovered child statuses. */ | ||
| readonly orchestrationTable: dynamodb.ITable; | ||
| /** TaskTable — read released children's task status (terminal? built?) + createTaskCore writes. */ | ||
| readonly taskTable: dynamodb.ITable; | ||
| /** TaskEventsTable — createTaskCore writes task_created events. */ | ||
| readonly taskEventsTable: dynamodb.ITable; | ||
| /** Orchestrator function ARN — releaseChild → createTaskCore async-invokes it. */ | ||
| readonly orchestratorFunctionArn?: string; | ||
| /** | ||
| * Sweep cadence. Long enough to amortise the scan; short enough to | ||
| * clear a lost-event stall in a reasonable user-facing time. | ||
| * @default Duration.minutes(10) | ||
| */ | ||
| readonly schedule?: Duration; | ||
| } | ||
|
|
||
| /** | ||
| * Scheduled backstop for sub-issue orchestration. | ||
| * | ||
| * The live ``OrchestrationReconciler`` reacts to TaskTable-stream terminal | ||
| * events to release dependency-unblocked children. If it is unavailable | ||
| * when an event fires (deploy/throttle/OOM/DLQ-parked record) that event | ||
| * is lost and the orchestration stalls. This scheduled sweep re-derives | ||
| * gating truth from persisted state and recovers stranded children | ||
| * (see ``handlers/reconcile-stranded-orchestrations.ts``). | ||
| * | ||
| * Mirrors ``StrandedTaskReconciler``. Grants match the live reconciler | ||
| * because it runs the same ``createTaskCore`` release path in-process. | ||
| */ | ||
|
|
||
| /** Sweep Lambda timeout (minutes) — matches the live reconciler's createTaskCore | ||
| * + Bedrock/S3 SDK bundle cold-start + release work. */ | ||
| const SWEEP_TIMEOUT_MINUTES = 5; | ||
|
|
||
| export class StrandedOrchestrationReconciler extends Construct { | ||
| public readonly fn: lambda.NodejsFunction; | ||
|
|
||
| constructor(scope: Construct, id: string, props: StrandedOrchestrationReconcilerProps) { | ||
| super(scope, id); | ||
|
|
||
| const handlersDir = path.join(__dirname, '..', 'handlers'); | ||
|
|
||
| this.fn = new lambda.NodejsFunction(this, 'ReconcilerFn', { | ||
| entry: path.join(handlersDir, 'reconcile-stranded-orchestrations.ts'), | ||
| handler: 'handler', | ||
| runtime: Runtime.NODEJS_24_X, | ||
| architecture: Architecture.ARM_64, | ||
| timeout: Duration.minutes(SWEEP_TIMEOUT_MINUTES), | ||
| // 512 MB to match the live reconciler — same createTaskCore + | ||
| // Bedrock/S3 SDK bundle (see OrchestrationReconciler memory note). | ||
| memorySize: 512, | ||
| environment: { | ||
| ORCHESTRATION_TABLE_NAME: props.orchestrationTable.tableName, | ||
| TASK_TABLE_NAME: props.taskTable.tableName, | ||
| TASK_EVENTS_TABLE_NAME: props.taskEventsTable.tableName, | ||
| ...(props.orchestratorFunctionArn && { | ||
| ORCHESTRATOR_FUNCTION_ARN: props.orchestratorFunctionArn, | ||
| }), | ||
| }, | ||
| bundling: { | ||
| externalModules: ['@aws-sdk/*'], | ||
| // Imports createTaskCore (releases children, which can carry attachments) — | ||
| // its transitive attachment-screening PDF path needs pdf-parse unbundled. | ||
| // Kept in lockstep with the other attachment-screening constructs; see the | ||
| // //:check:pdf-parse-bundling guard that enforces this. | ||
| nodeModules: ['pdf-parse'], | ||
| }, | ||
| }); | ||
|
|
||
| props.orchestrationTable.grantReadWriteData(this.fn); | ||
| props.taskTable.grantReadWriteData(this.fn); | ||
| props.taskEventsTable.grantReadWriteData(this.fn); | ||
|
|
||
| const schedule = props.schedule ?? Duration.minutes(10); | ||
| const rule = new events.Rule(this, 'SweepSchedule', { | ||
| schedule: events.Schedule.rate(schedule), | ||
| }); | ||
| rule.addTarget(new targets.LambdaFunction(this.fn)); | ||
|
|
||
| NagSuppressions.addResourceSuppressions(this.fn, [ | ||
| { | ||
| id: 'AwsSolutions-IAM4', | ||
| reason: 'AWSLambdaBasicExecutionRole is required for CloudWatch Logs access', | ||
| }, | ||
| { | ||
| id: 'AwsSolutions-IAM5', | ||
| reason: | ||
| 'DynamoDB index/* wildcards generated by CDK grantReadWriteData for the ' | ||
| + 'orchestration scan + child-task lookups + createTaskCore write path', | ||
| }, | ||
| ], true); | ||
| } | ||
| } |
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.