@@ -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+
137184func 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+
711805func (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