Skip to content

SY-4360: Task Config Provider Registry and Wire Composition#2472

Draft
emilbon99 wants to merge 1 commit into
rcfrom
sy-4360-task-config-split-provider-registry-and-wire-composition
Draft

SY-4360: Task Config Provider Registry and Wire Composition#2472
emilbon99 wants to merge 1 commit into
rcfrom
sy-4360-task-config-split-provider-registry-and-wire-composition

Conversation

@emilbon99

@emilbon99 emilbon99 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Issue Pull Request

Linear Issue

SY-4360

Description

Phase 1 of the task/config split (RFC 0042, #2471): the type-agnostic machinery that
lets per-integration config services plug into the task service. Inert in production:
no providers are registered, so behavior is byte-identical to today.

  • ConfigProvider interface + registry (core/pkg/service/task/config.go).
    Providers register exact task-type strings (mirroring imex's per-subtype importers);
    duplicate registration errors. All provider methods receive the task writer's
    gorp.Tx, so a task and its config are created, copied, or deleted in one
    transaction.
  • Writer dispatch. Create routes the effective config (including the
    snapshot-preserved config on updates) to the matching provider and defines the
    task -> parent -> config ontology relationship. Delete and Copy route through
    the provider. Internal tasks and task types without a provider take the existing
    embedded-blob path unchanged. The blob keeps being written alongside the typed
    record (dual-write), per the RFC's downgrade-safe rollout.
  • Wire composition. task.Service.ResolveConfigs fills Task.Config from the
    provider on the API retrieve path, falling back to the stored blob when the provider
    has no record, so mixed states (pre-bootstrap tasks) resolve correctly.
  • DecodeConfig helper. The newest-first decode chain providers will use to
    normalize old-shaped wire input through the same versioned transforms as storage
    migration.

Covered by a fake-provider Ginkgo suite (registration conflicts, dispatch, internal
exclusion, fallback, snapshot preservation, delete/copy routing, resolution fallback,
decode chain). Task, driver, and rack suites pass.

Basic Readiness

  • I have performed a self-review of my code.
  • I have added relevant, automated tests to cover the changes.
  • I have updated documentation to reflect the changes.

Greptile Summary

This PR introduces Phase 1 of the task/config split (RFC 0042): a ConfigProvider interface and registry that lets per-integration config services plug into the task service, with writer dispatch for Create/Delete/Copy and a ResolveConfigs read path that fills Task.Config from the provider, falling back to the stored blob for unregistered types and pre-bootstrap tasks. Because no providers are registered yet, runtime behavior is byte-identical to the current state.

  • ConfigProvider + registry (config.go): thread-safe registration with duplicate-type detection; ResolveConfigs skips internal tasks, unknown types, and tasks whose provider returns ErrNotFound, correctly handling mixed migration states.
  • Writer dispatch (writer.go): Create captures the snapshot-preserved config before gorp write and routes it to the provider; Delete pre-fetches the task to determine its type and internal flag before dispatching; Copy links the duplicated config resource to the new task's ontology node.
  • DecodeConfig helper (config.go): generic newest-first decode chain returning the first success or the first (current-schema) error, with ErrNoConfigDecoders for the zero-decoder case; backed by a thorough Ginkgo suite.

Confidence Score: 4/5

Safe to merge as written — no providers are registered in production yet, so all new dispatch paths are unreachable and runtime behavior is unchanged.

The writer's Copy path has no fallback for query.ErrNotFound from p.Copy, unlike ResolveConfigs which explicitly handles pre-bootstrap tasks. Once providers go live in a later phase, copying an unbootstrapped source task will fail rather than silently falling back to the stored blob.

core/pkg/service/task/writer.go — specifically the Copy function's provider dispatch block.

Important Files Changed

Filename Overview
core/pkg/service/task/config.go Introduces ConfigProvider interface, configProviders registry (with RWMutex for concurrent safety), ResolveConfigs with ErrNotFound fallback for mixed-state reads, and DecodeConfig generic helper. All logic is well-structured and properly handles edge cases.
core/pkg/service/task/writer.go Wires provider dispatch into Create (with snapshot-preserved config capture), Delete (with pre-lookup and internal guard), and Copy (config duplication + ontology linking). Copy lacks the ErrNotFound fallback present in ResolveConfigs, which could surface failures for pre-bootstrap source tasks when providers go live.
core/pkg/service/task/config_test.go Comprehensive Ginkgo suite covering registration conflicts, dispatch, internal exclusion, snapshot preservation, ErrNotFound fallback in ResolveConfigs, delete/copy routing, and DecodeConfig chain. No missing coverage for the tested paths.
core/pkg/service/task/service.go Minimal diff: adds configProviders field to Service and passes &s.configProviders into NewWriter. Clean and correct.
core/pkg/api/task/task.go Adds ResolveConfigs call after the retrieve query and before status augmentation, correctly using nil tx (falls back to DB). Single-line addition is correct and well-placed.

Sequence Diagram

sequenceDiagram
    participant API as api/task
    participant W as Writer
    participant T as gorp.Table
    participant P as ConfigProvider
    participant O as Ontology

    note over API,O: Create / Update path
    API->>W: Create(ctx, task)
    W->>T: NewCreate + MergeExisting (snapshot cfg preserved)
    T-->>W: merged task (cfg captured)
    W->>O: HasResource(taskID)
    alt first creation
        W->>O: DefineResource(taskID)
        W->>O: DefineRelationship(group → task)
    end
    alt "provider registered && not internal"
        W->>P: Create(ctx, tx, key, type, cfg)
        P-->>W: configID
        W->>O: DefineRelationship(task → config)
    end

    note over API,O: Retrieve path (ResolveConfigs)
    API->>W: Retrieve query
    W-->>API: []Task (with stored blobs)
    API->>P: ResolveConfigs(ctx, nil, tasks)
    loop each non-internal task with provider
        P->>P: Load(ctx, tx, key)
        alt found
            P-->>API: cfg (overrides stored blob)
        else ErrNotFound
            note over API: fallback: keep stored blob
        end
    end

    note over API,O: Delete path
    API->>W: Delete(ctx, key, allowInternal)
    W->>T: Retrieve(key) → task
    alt "task found && not internal && provider exists"
        W->>P: Delete(ctx, tx, key)
    end
    W->>T: Delete(key)
    W->>O: DeleteResource(taskID)

    note over API,O: Copy path
    API->>W: Copy(ctx, key, name, snapshot)
    W->>T: NewUpdate (clones entry with newKey)
    W->>O: DefineResource(newTaskID)
    alt "not internal && provider exists"
        W->>P: Copy(ctx, tx, from, to)
        P-->>W: configID
        W->>O: DefineRelationship(newTask → config)
    end
Loading

Reviews (1): Last reviewed commit: "SY-4360: Task config provider registry a..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.73077% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.64%. Comparing base (4205b3e) to head (dc2c0ac).
⚠️ Report is 2 commits behind head on rc.

Files with missing lines Patch % Lines
core/pkg/service/task/writer.go 64.58% 7 Missing and 10 partials ⚠️
core/pkg/service/task/config.go 95.91% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##               rc    #2472      +/-   ##
==========================================
+ Coverage   67.60%   67.64%   +0.03%     
==========================================
  Files        2528     2529       +1     
  Lines      121829   122061     +232     
  Branches     8995     9011      +16     
==========================================
+ Hits        82364    82569     +205     
- Misses      33030    33049      +19     
- Partials     6435     6443       +8     
Flag Coverage Δ
client-py 86.01% <ø> (+0.02%) ⬆️
client-ts 92.27% <ø> (ø)
console 30.83% <ø> (ø)
core 71.78% <81.73%> (+0.02%) ⬆️
pluto 60.55% <ø> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment on lines +180 to 196
p, ok := w.providers.get(res.Type)
if !ok {
return res, nil
}
configID, err := p.Copy(ctx, w.tx, key, newKey)
if err != nil {
return Task{}, err
}
if err = w.otg.DefineRelationship(
ctx,
OntologyID(newKey),
ontology.RelationshipTypeParentOf,
configID,
); err != nil {
return Task{}, err
}
return res, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Copy doesn't handle pre-bootstrap ErrNotFound from provider

ResolveConfigs explicitly swallows query.ErrNotFound from p.Load, so pre-bootstrap tasks (those whose config hasn't been migrated to a provider yet) resolve correctly. Copy has no equivalent fallback: if a provider is registered and the source task has no config record yet, p.Copy returns query.ErrNotFound, and this surfaces as an error from Writer.Copy. During a live migration window — where some tasks have config records and others don't — copying an unbootstrapped task will fail even though it would succeed for tasks without a registered provider.

@emilbon99 emilbon99 marked this pull request as draft June 12, 2026 17:32
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