Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions api/event/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,8 @@ type (
Path = string

// Event is the fundamental structure representing an event.
// Aux carries in-process auxiliary context alongside Data — dispatcher
// facts about the event that are not the event's payload. It never
// crosses a process boundary and consumers that do not know it ignore it.
Event struct {
Data any
Aux any
System System
Kind Kind
Path Path
Expand Down
32 changes: 20 additions & 12 deletions api/modules/sources.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,16 @@ type Source struct {
// Sources maps stable source identifiers to their current load identities.
type Sources map[string]Source

// LoadedSources is one atomic view of the normalized deployment baseline, its
// provenance, and the sources authoritative over its entries.
// LoadedSources is one atomic view of the normalized deployment baseline and
// the sources authoritative over its entries.
type LoadedSources struct {
Provenance regapi.ProvenanceMap
Owners []string
Entries []regapi.Entry
Owners []string
Entries []regapi.Entry
}

// SourceLoader rebuilds the normalized deployment baseline and its provenance
// from an atomic snapshot of its current sources.
type SourceLoader func(context.Context, Sources) ([]regapi.Entry, regapi.ProvenanceMap, error)
// SourceLoader rebuilds the normalized deployment baseline from an atomic
// snapshot of its current sources.
type SourceLoader func(context.Context, Sources) ([]regapi.Entry, error)

// SourceRegistry owns deployment source identities and coordinates source
// reloads with backing-store transitions.
Expand Down Expand Up @@ -234,6 +233,16 @@ func (r *SourceRegistry) SetLoader(loader SourceLoader) {
r.mu.Unlock()
}

// Snapshot returns a detached view of the current deployment sources.
func (r *SourceRegistry) Snapshot() Sources {
if r == nil {
return nil
}
r.mu.RLock()
defer r.mu.RUnlock()
return cloneSources(r.sources)
}

// Load rebuilds the normalized deployment baseline from one stable source
// generation.
func (r *SourceRegistry) Load(ctx context.Context) (LoadedSources, error) {
Expand All @@ -249,14 +258,13 @@ func (r *SourceRegistry) Load(ctx context.Context) (LoadedSources, error) {
if loader == nil {
return LoadedSources{}, ErrSourceLoaderUnavailable
}
entries, prov, err := loader(ctx, sources)
entries, err := loader(ctx, sources)
if err != nil {
return LoadedSources{}, err
}
return LoadedSources{
Owners: authoritativeOwners(sources),
Entries: entries,
Provenance: prov,
Owners: authoritativeOwners(sources),
Entries: entries,
}, nil
}

Expand Down
39 changes: 26 additions & 13 deletions api/modules/sources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestSourceRegistryLoadAndResourceRoots(t *testing.T) {
},
})

registry.SetLoader(func(context.Context, Sources) ([]regapi.Entry, regapi.ProvenanceMap, error) { return nil, nil, nil })
registry.SetLoader(func(context.Context, Sources) ([]regapi.Entry, error) { return nil, nil })
loaded, err := registry.Load(context.Background())
if err != nil {
t.Fatal(err)
Expand All @@ -45,14 +45,27 @@ func TestSourceRegistryLoadAndResourceRoots(t *testing.T) {
}
}

func TestSourceRegistrySnapshotIsDetached(t *testing.T) {
registry := NewSourceRegistry()
registry.Set(Sources{"acme/app": {LoadPath: "/pack", Version: "1.0.0"}})

snapshot := registry.Snapshot()
snapshot["acme/app"] = Source{LoadPath: "/changed"}
delete(snapshot, "acme/app")

if got := registry.Snapshot()["acme/app"].LoadPath; got != "/pack" {
t.Fatalf("stored source changed through snapshot: %q", got)
}
}

func TestSourceLoaderReceivesIsolatedSnapshot(t *testing.T) {
registry := NewSourceRegistry()
source := Source{LoadPath: "/repo/ui", Owner: "acme/ui", Version: "1.2.3", Digest: "sha256:ui", Sequence: 1}
registry.Set(Sources{"acme/ui": source})
var loaded Sources
registry.SetLoader(func(_ context.Context, sources Sources) ([]regapi.Entry, regapi.ProvenanceMap, error) {
registry.SetLoader(func(_ context.Context, sources Sources) ([]regapi.Entry, error) {
loaded = sources
return []regapi.Entry{{ID: regapi.NewID("example", "entry"), Kind: "registry.entry"}}, nil, nil
return []regapi.Entry{{ID: regapi.NewID("example", "entry"), Kind: "registry.entry"}}, nil
})

result, err := registry.Load(context.Background())
Expand All @@ -63,11 +76,11 @@ func TestSourceLoaderReceivesIsolatedSnapshot(t *testing.T) {
t.Fatalf("load = %v, %#v", result.Entries, loaded)
}
loaded["acme/ui"] = Source{LoadPath: "/mutated"}
registry.SetLoader(func(_ context.Context, sources Sources) ([]regapi.Entry, regapi.ProvenanceMap, error) {
registry.SetLoader(func(_ context.Context, sources Sources) ([]regapi.Entry, error) {
if sources["acme/ui"] != source {
t.Fatalf("loader mutated registry: %#v", sources)
}
return nil, nil, nil
return nil, nil
})
_, err = registry.Load(context.Background())
if err != nil {
Expand All @@ -84,22 +97,22 @@ func TestSourceRegistrySetReplacesCompleteSnapshot(t *testing.T) {
registry.Set(Sources{
ApplicationSourceID: {LoadPath: "/repo/next", Owner: ApplicationSourceID},
})
registry.SetLoader(func(_ context.Context, sources Sources) ([]regapi.Entry, regapi.ProvenanceMap, error) {
registry.SetLoader(func(_ context.Context, sources Sources) ([]regapi.Entry, error) {
if len(sources) != 1 || sources[ApplicationSourceID].LoadPath != "/repo/next" {
t.Fatalf("sources = %#v", sources)
}
return nil, nil, nil
return nil, nil
})
if _, err := registry.Load(context.Background()); err != nil {
t.Fatal(err)
}

registry.Set(nil)
registry.SetLoader(func(_ context.Context, sources Sources) ([]regapi.Entry, regapi.ProvenanceMap, error) {
registry.SetLoader(func(_ context.Context, sources Sources) ([]regapi.Entry, error) {
if len(sources) != 0 {
t.Fatalf("sources after empty set = %#v", sources)
}
return nil, nil, nil
return nil, nil
})
if _, err := registry.Load(context.Background()); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -142,10 +155,10 @@ func TestSourceTransitionWaitsForActiveReload(t *testing.T) {

loadStarted := make(chan Sources, 1)
releaseLoad := make(chan struct{})
registry.SetLoader(func(_ context.Context, sources Sources) ([]regapi.Entry, regapi.ProvenanceMap, error) {
registry.SetLoader(func(_ context.Context, sources Sources) ([]regapi.Entry, error) {
loadStarted <- sources
<-releaseLoad
return nil, nil, nil
return nil, nil
})
loadDone := make(chan error, 1)
go func() {
Expand Down Expand Up @@ -194,11 +207,11 @@ func TestSourceTransitionFailureKeepsPreviousIdentity(t *testing.T) {
if err == nil || len(previous) != 0 {
t.Fatalf("failed transition = %#v, %v", previous, err)
}
registry.SetLoader(func(_ context.Context, sources Sources) ([]regapi.Entry, regapi.ProvenanceMap, error) {
registry.SetLoader(func(_ context.Context, sources Sources) ([]regapi.Entry, error) {
if sources["acme/ui"] != oldSource {
t.Fatalf("source changed after failure: %#v", sources)
}
return nil, nil, nil
return nil, nil
})
_, err = registry.Load(context.Background())
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion api/registry/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,12 @@ func (m *mockRegistry) Apply(context.Context, ChangeSet) (Version, error) {
return nil, nil //nolint:nilnil // test mock
}
func (m *mockRegistry) ApplyVersion(context.Context, Version) error { return nil }
func (m *mockRegistry) LoadState(context.Context, ProvenancedState, Version) error {
func (m *mockRegistry) LoadState(context.Context, State, Version) error {
return nil
}
func (m *mockRegistry) Current() (Version, error) { return nil, nil } //nolint:nilnil // test mock
func (m *mockRegistry) History() History { return nil }
func (m *mockRegistry) Snapshot() Snapshot { return Snapshot{} }
func (m *mockRegistry) RegisterDependencyPattern(_ DependencyPattern) error {
return nil
}
Expand Down
18 changes: 7 additions & 11 deletions api/registry/expansion.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,16 @@ type ScopedOperation struct {
type DirectiveResult struct {
OriginalScope *Scope
Resolution *DependencyResolution
// Provenance updates resident records for entries the directive emits no
// operation for: a module version bump whose entries are byte-identical
// advances their resident identity here, with no entry event.
Provenance ProvenanceMap
Additional []ScopedOperation
Effects []Effect
Applied bool
Additional []ScopedOperation
Effects []Effect
Applied bool
}

// ResolutionDirective restores derived state from the exact dependency graph
// stored for a registry version. It is used once after declarative history has
// been reconstructed, never once per historical changeset.
type ResolutionDirective interface {
ReconcileResolution(context.Context, ProvenancedState, *DependencyResolution) (DirectiveResult, error)
ReconcileResolution(context.Context, State, *DependencyResolution) (DirectiveResult, error)
}

// ResolutionTransitionDirective is the transition-aware form of
Expand All @@ -46,21 +42,21 @@ type ResolutionDirective interface {
// prefers this interface when implemented and falls back to
// ResolutionDirective for compatibility.
type ResolutionTransitionDirective interface {
ReconcileResolutionTransition(context.Context, ProvenancedState, ProvenancedState, *DependencyResolution) (DirectiveResult, error)
ReconcileResolutionTransition(context.Context, State, State, *DependencyResolution) (DirectiveResult, error)
}

// ChangesDirective expands all same-kind operations in one resolution pass.
// It prevents a multi-root transaction from staging intermediate graphs.
type ChangesDirective interface {
ExpandChanges(context.Context, ChangeSet, ProvenancedState) (DirectiveResult, error)
ExpandChanges(context.Context, ChangeSet, State) (DirectiveResult, error)
}

// Directive can augment a registry operation with additional operations or effects.
// Implementations may perform external work but must honor the provided context.
// Directives must not call Apply/ApplyVersion/LoadState (Apply is not re-entrant).
// Use Effects for work that must be staged, committed, or rolled back alongside Apply.
type Directive interface {
Expand(ctx context.Context, op Operation, snapshot ProvenancedState) (DirectiveResult, error)
Expand(ctx context.Context, op Operation, snapshot State) (DirectiveResult, error)
}

// Effect represents external work tied to an expanded operation.
Expand Down
Loading