Skip to content

[EXPERIMENTAL] feat(dag): migrate DAG support to extension SPI - #611

Draft
zhongkechen wants to merge 3 commits into
codex/extension-operation-refactorfrom
codex/experimental-dag-extension-spi
Draft

[EXPERIMENTAL] feat(dag): migrate DAG support to extension SPI#611
zhongkechen wants to merge 3 commits into
codex/extension-operation-refactorfrom
codex/experimental-dag-extension-spi

Conversation

@zhongkechen

@zhongkechen zhongkechen commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Warning

Experimental stacked PR. This work depends on draft PR #607 and is not intended for production use. The DAG API, checkpoint shape, and implementation may change before merge or release.

Issue Link, if available

Stacked on #607.

Description

Migrates the experimental DAG implementation to the extension operation SPI introduced by #607.

  • Adds static DurableDagOperation.dag(...) and dagAsync(...) entry points following the new Durable*Operation facade style.
  • Removes DAG-specific methods and internal hooks from DurableContext.
  • Schedules DAG containers and tasks through ExtensionContext and ExtensionOperation.
  • Preserves stable name-based DAG task IDs.
  • Reuses unchanged map, parallel, and wait-for-condition facades through a DAG-internal reserved-context adapter.
  • Leaves all other Durable*Operation classes and their implementation tests unchanged from the stacked base.
  • Handles large DAG results with extension child replay state.
  • Updates examples, conformance handlers, integration tests, and DAG documentation.

Demo/Screenshots

Not applicable. This is an SDK API and execution implementation change.

Checklist

  • I have filled out every section of the PR template
  • I have thoroughly tested this change

Testing

Unit Tests

Yes. Added focused coverage for DurableDagOperation and the DAG-internal reserved-context adapter.

Integration Tests

Yes. DAG integration and conformance tests were updated for the static operation facade. The complete Maven reactor passes.

Commands run:

mvn spotless:apply
mvn -pl sdk -Dtest=DurableDagOperationTest,ReservedOperationContextTest test \
  -DargLine=-javaagent:$HOME/.m2/repository/org/mockito/mockito-core/5.23.0/mockito-core-5.23.0.jar
mvn test \
  -DargLine=-javaagent:$HOME/.m2/repository/org/mockito/mockito-core/5.23.0/mockito-core-5.23.0.jar

Cloud example tests remain disabled by default.

Examples

Existing DAG examples were migrated to DurableDagOperation; no additional example was required.

@zhongkechen
zhongkechen deployed to ai-pr-review August 10, 2026 17:29 — with GitHub Actions Active
@zhongkechen
zhongkechen requested a deployment to ai-pr-review-runtime August 10, 2026 17:29 — with GitHub Actions Waiting
@zhongkechen
zhongkechen had a problem deploying to ai-pr-review-runtime August 10, 2026 17:29 — with GitHub Actions Failure
@zhongkechen
zhongkechen force-pushed the codex/experimental-dag-extension-spi branch 2 times, most recently from ecf0c30 to e25ea9c Compare August 10, 2026 20:05
@zhongkechen
zhongkechen force-pushed the codex/experimental-dag-extension-spi branch 2 times, most recently from 25cf931 to b08588f Compare August 10, 2026 23:59
@zhongkechen
zhongkechen force-pushed the codex/experimental-dag-extension-spi branch from 6a8bbc1 to 818b256 Compare August 11, 2026 05:19
@zhongkechen
zhongkechen deployed to ai-pr-review August 14, 2026 21:52 — with GitHub Actions Active
@zhongkechen
zhongkechen had a problem deploying to ai-pr-review-runtime August 14, 2026 21:52 — with GitHub Actions Failure
@zhongkechen
zhongkechen temporarily deployed to ai-pr-review-runtime August 14, 2026 21:52 — with GitHub Actions Inactive
Comment on lines +84 to +90
while (!inFlight.isEmpty()) {
var it = inFlight.entrySet().iterator();
var entry = it.next();
it.remove();
String name = entry.getKey();
try {
Object result = entry.getValue().future.get();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review

[P1] Completion is observed in launch order. If an earlier task is a long wait/callback while a later task satisfies firstSuccessful or a custom policy, the DAG still blocks on the earlier future. Skips also bypass completion evaluation. Await any settled future and evaluate the policy after every terminal transition, persisting enough state to replay the same completion boundary.

Comment on lines +206 to +208
return switch (kind) {
case PLAIN -> rehydratePlain(raw, taskName, scope);
case BATCH -> delegate.deserialize(delegate.serialize(raw), TypeToken.get(MapResult.class));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review

[P1] Deserializing raw MapResult.class erases O. A MapResult<MyRecord> is normalized into MapResult<LinkedHashMap>, causing typed access to fail even on first execution. Retain the map output TypeToken and reconstruct a parameterized MapResult or each item with that type; add a POJO replay test.

Comment on lines +92 to +95
} catch (UnrecoverableDurableExecutionException e) {
throw e;
} catch (RuntimeException e) {
results.put(name, failed(name, DagTaskError.of(e)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review

[P2] Durable futures may rethrow the original non-RuntimeException Throwable. Such a task currently aborts the DAG after its operation was checkpointed failed, bypassing compensation. Explicitly rethrow SuspendExecutionException and UnrecoverableDurableExecutionException, then catch remaining Throwable as a failed task.

Comment on lines +257 to +261
var cc = unwrap(dcc);
int succeeded = countByStatus(results, TaskStatus.SUCCEEDED);
int failed = countByStatus(results, TaskStatus.FAILED);
if (cc.minSuccessful() != null && succeeded >= cc.minSuccessful()) {
return DagCompletionReason.MIN_SUCCESSFUL_REACHED;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review

[P2] When minSuccessful exceeds the registered task count, the threshold can never fire and the scheduler silently returns ALL_COMPLETED or COMPLETED_WITH_FAILURES. Validate this after graph registration and before reserving operations, matching map's existing validation.

Comment on lines +227 to +230
try {
return delegate.deserialize(delegate.serialize(raw), declared.get());
} catch (RuntimeException e) {
return raw;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review

[P2] A known declared type must not silently degrade to a raw JSON tree when deserialization fails; this converts a serialization error into a delayed ClassCastException and defeats deserialize-after-serialize validation. Propagate the SerDes failure, reserving raw fallback only for unknown task names.

Comment on lines +62 to +96
// ── invoke ───────────────────────────────────────────────────────────────
<T> TaskHandle<T> invoke(String name, String functionName, Class<T> type, DagPayloadFunction payloadFn);

<T> TaskHandle<T> invoke(
String name, String functionName, Class<T> type, DagPayloadFunction payloadFn, InvokeConfig config);

// ── callback ─────────────────────────────────────────────────────────────
<T> TaskHandle<T> callback(String name, Class<T> type, DagCallbackSubmitter submitter);

<T> TaskHandle<T> callback(
String name, Class<T> type, DagCallbackSubmitter submitter, WaitForCallbackConfig config);

// ── wait ─────────────────────────────────────────────────────────────────
TaskHandle<Void> wait(String name, Duration duration);

// ── waitForCondition ──────────────────────────────────────────────────────
<S> TaskHandle<S> waitForCondition(
String name, Class<S> type, DagConditionFunction<S> check, WaitForConditionConfig<S> config);

// ── runInChildContext ─────────────────────────────────────────────────────
<T> TaskHandle<T> runInChildContext(String name, Class<T> type, DagChildFunction<T> fn);

<T> TaskHandle<T> runInChildContext(String name, TypeToken<T> type, DagChildFunction<T> fn);

// ── map ──────────────────────────────────────────────────────────────────
<I, O> TaskHandle<MapResult<O>> map(String name, Collection<I> items, Class<O> type, MapFunction<I, O> fn);

<I, O> TaskHandle<MapResult<O>> map(
String name, Collection<I> items, Class<O> type, MapFunction<I, O> fn, MapConfig config);

<I, O> TaskHandle<MapResult<O>> map(
String name, Function<Deps, Collection<I>> items, Class<O> type, MapFunction<I, O> fn);

<I, O> TaskHandle<MapResult<O>> map(
String name, Function<Deps, Collection<I>> items, Class<O> type, MapFunction<I, O> fn, MapConfig config);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review

[P2] Unlike the core APIs, invoke, callback, waitForCondition, and map expose only Class result types, so generic results such as List<MyRecord> cannot be represented and replay as raw collections. Add corresponding TypeToken overloads and delegate the Class overloads to them.

Comment thread docs/core/dag.md
Comment on lines +134 to +148
## Completion (threshold only in v1)

`DagConfig.builder().completionConfig(...)` accepts one of six threshold policies:
`allCompleted`, `allSuccessful`, `firstSuccessful`, `minSuccessful(n)`, `toleratedFailureCount(n)`,
`toleratedFailurePercentage(p)`. Default (no `completionConfig`) drains the whole reachable graph. `completionReason()`
reports `ALL_COMPLETED`, `COMPLETED_WITH_FAILURES`, `MIN_SUCCESSFUL_REACHED`, or `FAILURE_TOLERANCE_EXCEEDED`.

> **v2-deferred:** Custom-predicate (result-based) completion is **not** in v1. `DagCompletionConfig` exposes only the
> threshold factories, and `DagCompletionReason.CUSTOM_COMPLETION_*` are reserved-but-unreachable.

## Results

`DagResult` provides `getResult(TaskHandle<T>) -> Optional<T>` (typed) and `getResult(String) -> Optional<Object>`
(untyped), `getStatus(...)`, grouped views (`succeeded()`/`failed()`/`skipped()`), counts, `completionReason()`, and
`throwIfError()` (throws `DagExecutionException` iff `failureCount() > 0`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review

[P2] This page documents a different API: custom completion is public and tested, its completion reasons are reachable, and throwIfError() also throws for CUSTOM_COMPLETION_FAILED. Earlier examples also reference nonexistent getOptional and treat Deps.get as returning T instead of Optional<T>. Rewrite these sections against the actual signatures and behavior so the examples compile.

@github-actions

Copy link
Copy Markdown

Codex AI review

Found blocking completion and serialization issues, plus API/error-handling gaps and inaccurate documentation. Static review only; tests were not run per instruction.

Reviewed commit 818b256a1f80bfcdf7853d47f3853ac9a051f5bf. Workflow run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant