Skip to content

Commit b0b0553

Browse files
fix(duckdb): give local push-watch deferred scopes a polling owner (#1298)
Local DuckDBPushWatch mirrored pg watch's pre-fix gap: no sync engine, no unwatched-root poller, and watcher options only wired OnCoverageDegraded. Interval pushes used runLocalSyncAuthoritative → SyncAll, which never tombstones missed deletions under deferred scopes. Align local duckdb watch with local pg watch: shared watcher options, engine-backed batch sync, startup sync, and the probe-gated poller. Add TestLocalDuckDBPushWatchGivesDeferredScopesAPollingOwner parallel to the existing pg regression at archive_write_backend_test.go:821. Daemon-delegated duckdb/pg watch remains unchanged (no local engine). Co-authored-by: cyre <diazMelgarejo@users.noreply.github.com>
1 parent dc0a980 commit b0b0553

3 files changed

Lines changed: 333 additions & 69 deletions

File tree

cmd/agentsview/archive_write_backend.go

Lines changed: 144 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@ type archivePushWatchHooks struct {
7474
pgStartupSync func(
7575
context.Context, *syncpkg.Engine, bool,
7676
) (bool, error)
77+
duckDBStartupSync func(
78+
context.Context, *syncpkg.Engine, bool,
79+
) (bool, error)
7780
newPGPusher func(*syncpkg.Engine) *pgPusher
81+
newDuckDBPusher func(*syncpkg.Engine) *duckDBPusher
7882
newUnwatchedPoller func(context.Context, unwatchedPollSyncer) unwatchedRootPoller
7983
}
8084

@@ -87,8 +91,8 @@ type unwatchedRootPoller interface {
8791
Stop()
8892
}
8993

90-
// newArchivePushUnwatchedPoller builds the pg watch polling owner for
91-
// deferred scopes. The watcher's full recovery and rename promotion defer
94+
// newArchivePushUnwatchedPoller builds the archive push-watch polling owner
95+
// for deferred scopes. The watcher's full recovery and rename promotion defer
9296
// unavailable scopes to their polling probes, and the interval push runs a
9397
// plain SyncAll that never tombstones missed deletions, so without this owner
9498
// a deletion lost while a root was unavailable would stay active in the
@@ -134,6 +138,49 @@ func newArchivePushLoop(
134138
return loop, ticker.Stop
135139
}
136140

141+
func archivePushWatchWatcherOptions(
142+
loop *pushLoop, poller unwatchedRootPoller,
143+
) syncpkg.WatcherOptions {
144+
return syncpkg.WatcherOptions{
145+
OnCoverageDegraded: func(roots []string) error {
146+
// Degraded coverage needs both owners: the poller reconciles
147+
// the affected roots authoritatively (including tombstoning
148+
// missed deletions) and the loop re-pushes the refreshed
149+
// archive on its floor.
150+
if err := poller.AddObligation(pollingObligation{
151+
Key: "watcher-fallback", Roots: roots,
152+
}); err != nil {
153+
return err
154+
}
155+
return loop.NotifyCoverageDegraded(roots)
156+
},
157+
OnPollingRequired: func(obligation syncpkg.PollingObligation) error {
158+
return poller.AddObligation(pollingObligation{
159+
Key: obligation.Key,
160+
Roots: obligation.Roots,
161+
Probe: obligation.Probe,
162+
})
163+
},
164+
OnPollingReleased: poller.RemoveObligation,
165+
}
166+
}
167+
168+
func archivePushWatchBatchCallback(
169+
appCfg config.Config,
170+
engine *syncpkg.Engine,
171+
loop *pushLoop,
172+
) syncpkg.WatchCallback {
173+
return func(callbackCtx context.Context, batch syncpkg.WatchBatch) error {
174+
scope := func() watchRecoveryScope {
175+
return probeWatchRecoveryScope(appCfg)
176+
}
177+
if err := syncWatchBatch(callbackCtx, engine, batch, scope); err != nil {
178+
return err
179+
}
180+
return notifyPushForWatchBatch(callbackCtx, loop, batch)
181+
}
182+
}
183+
137184
func completeDuckDBWatchPush(
138185
res duckdbsync.PushResult, reason pushReason,
139186
) error {
@@ -687,6 +734,22 @@ func (b *localArchiveWriteBackend) duckDBPush(
687734
forceFull := cfg.Full || didResync
688735

689736
fmt.Println("Starting DuckDB push...")
737+
return b.duckDBMirrorPush(
738+
ctx, duckCfg, cfg, projects, excludeProjects, forceFull,
739+
)
740+
}
741+
742+
func (b *localArchiveWriteBackend) duckDBMirrorPush(
743+
ctx context.Context,
744+
duckCfg config.DuckDBConfig,
745+
cfg DuckDBPushConfig,
746+
projects []string,
747+
excludeProjects []string,
748+
forceFull bool,
749+
) (duckdbsync.PushResult, error) {
750+
if err := duckdbsync.ValidatePushTarget(duckCfg); err != nil {
751+
return duckdbsync.PushResult{}, err
752+
}
690753
opts := duckdbsync.SyncOptions{
691754
Projects: projects,
692755
ExcludeProjects: excludeProjects,
@@ -708,6 +771,37 @@ func (b *localArchiveWriteBackend) duckDBPush(
708771
return result, nil
709772
}
710773

774+
func (b *localArchiveWriteBackend) newDuckDBPusher(
775+
engine *syncpkg.Engine,
776+
duckCfg config.DuckDBConfig,
777+
cfg DuckDBPushConfig,
778+
projects, exclude []string,
779+
) *duckDBPusher {
780+
pushCfg := cfg
781+
pushCfg.Automatic = true
782+
return &duckDBPusher{
783+
localSync: func(c context.Context) error {
784+
stats := engine.SyncAll(c, nil)
785+
if err := c.Err(); err != nil {
786+
return err
787+
}
788+
if !stats.AuthoritativeDiscoveryComplete() {
789+
return errors.New("local sync discovery incomplete")
790+
}
791+
engine.FlushSignals()
792+
return nil
793+
},
794+
ensurePricing: b.ensureCurrentPricing,
795+
mirrorPush: func(c context.Context, forceFull bool) (
796+
duckdbsync.PushResult, error,
797+
) {
798+
return b.duckDBMirrorPush(
799+
c, duckCfg, pushCfg, projects, exclude, forceFull,
800+
)
801+
},
802+
}
803+
}
804+
711805
func (b *localArchiveWriteBackend) DuckDBPushWatch(
712806
ctx context.Context,
713807
duckCfg config.DuckDBConfig,
@@ -723,53 +817,70 @@ func (b *localArchiveWriteBackend) DuckDBPushWatch(
723817
if debounce <= 0 {
724818
debounce = defaultWatchDebounce
725819
}
726-
push := func(pctx context.Context, reason pushReason, full bool) error {
727-
pushCfg := cfg
728-
pushCfg.Full = full
729-
// Watch pushes are automatic: a mirror held by a live serve
730-
// process defers instead of rebuilding the whole archive on
731-
// every changed batch, and archive-scale diagnostics are
732-
// skipped. Push ignores the defer behavior when full is set.
733-
pushCfg.Automatic = true
734-
var res duckdbsync.PushResult
735-
var err error
736-
if b.watchHooks != nil && b.watchHooks.duckDBPush != nil {
737-
res, err = b.watchHooks.duckDBPush(pctx, reason, full)
738-
} else {
739-
res, err = b.DuckDBPush(
740-
pctx, duckCfg, pushCfg, projects, exclude,
741-
)
742-
}
743-
if err != nil {
744-
return err
820+
for _, def := range parser.Registry {
821+
if !b.appCfg.IsUserConfigured(def.Type) {
822+
continue
745823
}
746-
return completeDuckDBWatchPush(res, reason)
824+
warnMissingDirs(b.appCfg.ResolveDirs(def.Type), string(def.Type))
825+
}
826+
cleanResyncTemp(b.appCfg.DBPath)
827+
828+
engine := syncpkg.NewEngine(b.database, syncpkg.EngineConfig{
829+
AgentDirs: b.appCfg.AgentDirs,
830+
IncludeCwdPrefixes: b.appCfg.SyncIncludeCwdPrefixes,
831+
Machine: b.appCfg.LocalMachineName,
832+
BlockedResultCategories: b.appCfg.ResultContentBlockedCategories,
833+
})
834+
defer engine.Close()
835+
836+
var pusher *duckDBPusher
837+
if b.watchHooks != nil && b.watchHooks.newDuckDBPusher != nil {
838+
pusher = b.watchHooks.newDuckDBPusher(engine)
839+
} else {
840+
pusher = b.newDuckDBPusher(engine, duckCfg, cfg, projects, exclude)
747841
}
842+
748843
loop, stopLoop := newArchivePushLoop(
749844
b.watchHooks,
750845
"duckdb watch", debounce, interval,
751846
func(c context.Context, r pushReason) error {
752-
return push(c, r, false)
847+
return pusher.push(c, r, false)
753848
},
754849
)
755850
defer stopLoop()
756851

852+
poller := newArchivePushUnwatchedPoller(ctx, b.watchHooks, engine)
853+
defer poller.Stop()
854+
757855
stopWatcher, openDispatch, unwatchedDirs := startArchivePushWatcher(
758-
b.watchHooks, b.appCfg, nil,
759-
func(callbackCtx context.Context, batch syncpkg.WatchBatch) error {
760-
return notifyPushForWatchBatch(callbackCtx, loop, batch)
761-
},
762-
syncpkg.WatcherOptions{OnCoverageDegraded: loop.NotifyCoverageDegraded},
856+
b.watchHooks, b.appCfg, engine,
857+
archivePushWatchBatchCallback(b.appCfg, engine, loop),
858+
archivePushWatchWatcherOptions(loop, poller),
763859
)
764860
defer stopWatcher()
765861
if len(unwatchedDirs) > 0 {
766862
log.Printf(
767-
"duckdb watch: %d root(s) not watched; relying on the %s floor for coverage",
768-
len(unwatchedDirs), interval,
863+
"duckdb watch: %d root(s) not watched; polling every %s",
864+
len(unwatchedDirs), unwatchedPollInterval,
769865
)
770866
}
771-
initialErr := push(ctx, reasonStartup, cfg.Full)
867+
868+
startupSync := runPGWatchStartupSync
869+
if b.watchHooks != nil && b.watchHooks.duckDBStartupSync != nil {
870+
startupSync = b.watchHooks.duckDBStartupSync
871+
}
872+
didResync, startupErr := startupSync(ctx, engine, cfg.Full)
873+
if startupErr != nil && errors.Is(startupErr, context.Canceled) {
874+
return nil
875+
}
876+
initialErr := startupErr
877+
if initialErr == nil {
878+
initialErr = pusher.push(ctx, reasonStartup, didResync)
879+
}
772880
if initialErr != nil {
881+
if errors.Is(initialErr, context.Canceled) && ctx.Err() != nil {
882+
return nil
883+
}
773884
log.Printf("duckdb watch: initial push failed: %v", initialErr)
774885
}
775886
completePushWatchStartup(ctx, initialErr, loop, openDispatch)
@@ -900,37 +1011,8 @@ func (b *localArchiveWriteBackend) PGPushWatch(
9001011

9011012
stopWatcher, openDispatch, unwatchedDirs := startArchivePushWatcher(
9021013
b.watchHooks, b.appCfg, engine,
903-
func(callbackCtx context.Context, batch syncpkg.WatchBatch) error {
904-
scope := func() watchRecoveryScope {
905-
return probeWatchRecoveryScope(b.appCfg)
906-
}
907-
if err := syncWatchBatch(callbackCtx, engine, batch, scope); err != nil {
908-
return err
909-
}
910-
return notifyPushForWatchBatch(callbackCtx, loop, batch)
911-
},
912-
syncpkg.WatcherOptions{
913-
OnCoverageDegraded: func(roots []string) error {
914-
// Degraded coverage needs both owners: the poller reconciles
915-
// the affected roots authoritatively (including tombstoning
916-
// missed deletions) and the loop re-pushes the refreshed
917-
// archive on its floor.
918-
if err := poller.AddObligation(pollingObligation{
919-
Key: "watcher-fallback", Roots: roots,
920-
}); err != nil {
921-
return err
922-
}
923-
return loop.NotifyCoverageDegraded(roots)
924-
},
925-
OnPollingRequired: func(obligation syncpkg.PollingObligation) error {
926-
return poller.AddObligation(pollingObligation{
927-
Key: obligation.Key,
928-
Roots: obligation.Roots,
929-
Probe: obligation.Probe,
930-
})
931-
},
932-
OnPollingReleased: poller.RemoveObligation,
933-
},
1014+
archivePushWatchBatchCallback(b.appCfg, engine, loop),
1015+
archivePushWatchWatcherOptions(loop, poller),
9341016
)
9351017
defer stopWatcher()
9361018
if len(unwatchedDirs) > 0 {

0 commit comments

Comments
 (0)