From c1f19d3d1518a57cf861369991b9da8a6428fdb6 Mon Sep 17 00:00:00 2001 From: Yuan Teoh Date: Thu, 12 Feb 2026 15:31:00 -0800 Subject: [PATCH] feat: add polling system to dynamic reloading --- cmd/root.go | 61 +++++++++++++++++++++++++++++++++++++-- cmd/root_test.go | 2 +- docs/en/reference/cli.md | 11 +++++++ internal/server/config.go | 2 ++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 3d62c11dc8..71c5caa820 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -401,6 +401,7 @@ func NewCommand(opts ...Option) *Command { // TODO: Insecure by default. Might consider updating this for v1.0.0 flags.StringSliceVar(&cmd.cfg.AllowedOrigins, "allowed-origins", []string{"*"}, "Specifies a list of origins permitted to access this server. Defaults to '*'.") flags.StringSliceVar(&cmd.cfg.AllowedHosts, "allowed-hosts", []string{"*"}, "Specifies a list of hosts permitted to access this server. Defaults to '*'.") + flags.IntVar(&cmd.cfg.PollInterval, "poll-interval", 0, "Specifies the polling frequency (seconds) for configuration file updates.") persistentFlags.StringSliceVar(&cmd.cfg.UserAgentMetadata, "user-agent-metadata", []string{}, "Appends additional metadata to the User-Agent.") // wrap RunE command so that we have access to original Command object @@ -791,8 +792,22 @@ func validateReloadEdits( return sourcesMap, authServicesMap, embeddingModelsMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, nil } +// Helper to check if a file has a newer ModTime than stored in the map +func checkModTime(path string, lastSeen map[string]time.Time) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + mTime := info.ModTime() + if mTime.After(lastSeen[path]) { + lastSeen[path] = mTime + return true + } + return false +} + // watchChanges checks for changes in the provided yaml tools file(s) or folder. -func watchChanges(ctx context.Context, watchDirs map[string]bool, watchedFiles map[string]bool, s *server.Server) { +func watchChanges(ctx context.Context, watchDirs map[string]bool, watchedFiles map[string]bool, s *server.Server, pollTickerSecond int) { logger, err := util.LoggerFromContext(ctx) if err != nil { panic(err) @@ -806,6 +821,17 @@ func watchChanges(ctx context.Context, watchDirs map[string]bool, watchedFiles m defer w.Close() + var pollTickerChan <-chan time.Time + if pollTickerSecond > 0 { + ticker := time.NewTicker(time.Duration(pollTickerSecond) * time.Second) + defer ticker.Stop() + pollTickerChan = ticker.C // Assign the channel + logger.DebugContext(ctx, fmt.Sprintf("NFS polling enabled every %v", pollTickerSecond)) + } else { + logger.DebugContext(ctx, "NFS polling disabled (interval is 0)") + } + + lastSeen := make(map[string]time.Time) watchingFolder := false var folderToWatch string @@ -844,6 +870,37 @@ func watchChanges(ctx context.Context, watchDirs map[string]bool, watchedFiles m case <-ctx.Done(): logger.DebugContext(ctx, "file watcher context cancelled") return + case <-pollTickerChan: + changed := false + if watchingFolder { + // Scan directory for any .yaml/.yml file changes + files, err := os.ReadDir(folderToWatch) + if err != nil { + logger.WarnContext(ctx, "error reading tools folder %s", err) + continue + } + for _, f := range files { + if !f.IsDir() && (strings.HasSuffix(f.Name(), ".yaml") || strings.HasSuffix(f.Name(), ".yml")) { + fullPath := filepath.Join(folderToWatch, f.Name()) + if checkModTime(fullPath, lastSeen) { + changed = true + } + } + } + } else { + files := slices.Collect(maps.Keys(watchedFiles)) + // Check specific files in watchedFiles map + for _, f := range files { + if checkModTime(f, lastSeen) { + changed = true + } + } + } + if changed { + logger.DebugContext(ctx, "NFS remote change detected via polling") + // once this timer runs out, it will trigger debounce.C + debounce.Reset(debounceDelay) + } case err, ok := <-w.Errors: if !ok { logger.WarnContext(ctx, "file watcher was closed unexpectedly") @@ -1192,7 +1249,7 @@ func run(cmd *Command) error { if isCustomConfigured && !cmd.cfg.DisableReload { watchDirs, watchedFiles := resolveWatcherInputs(cmd.tools_file, cmd.tools_files, cmd.tools_folder) // start watching the file(s) or folder for changes to trigger dynamic reloading - go watchChanges(ctx, watchDirs, watchedFiles, s) + go watchChanges(ctx, watchDirs, watchedFiles, s, cmd.cfg.PollInterval) } // wait for either the server to error out or the command's context to be canceled diff --git a/cmd/root_test.go b/cmd/root_test.go index f26bd1706a..9995427ac2 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -2024,7 +2024,7 @@ func TestSingleEdit(t *testing.T) { watchedFiles := map[string]bool{cleanFileToWatch: true} watchDirs := map[string]bool{watchDir: true} - go watchChanges(ctx, watchDirs, watchedFiles, mockServer) + go watchChanges(ctx, watchDirs, watchedFiles, mockServer, 0) // escape backslash so regex doesn't fail on windows filepaths regexEscapedPathFile := strings.ReplaceAll(cleanFileToWatch, `\`, `\\\\*\\`) diff --git a/docs/en/reference/cli.md b/docs/en/reference/cli.md index 11549c2830..c84f5dd8d4 100644 --- a/docs/en/reference/cli.md +++ b/docs/en/reference/cli.md @@ -28,6 +28,7 @@ description: > | | `--allowed-origins` | Specifies a list of origins permitted to access this server for CORs access. | `*` | | | `--allowed-hosts` | Specifies a list of hosts permitted to access this server to prevent DNS rebinding attacks. | `*` | | | `--user-agent-metadata` | Appends additional metadata to the User-Agent. | | +| | `--poll-interval` | Specifies the polling frequency (seconds) for configuration file updates. | `0` | | `-v` | `--version` | version for toolbox | | ## Sub Commands @@ -136,6 +137,16 @@ used at a time. Toolbox enables dynamic reloading by default. To disable, use the `--disable-reload` flag. +Use the `--polling-interval` flag to manually detect configuration file updates. +When the polling interval is `0`, the polling system is disabled. + +{{< notice tip >}} +FOr polling to be effective when running Kubernetes, PersistentVolume or +StorageClass must be set to refresh attributes rapidly by setting `actimeo=1`. +Actimeo setting determines the duration for which a client trusts its local +cache for file attributes. +{{< /notice >}} + ### Toolbox UI To launch Toolbox's interactive UI, use the `--ui` flag. This allows you to test diff --git a/internal/server/config.go b/internal/server/config.go index 48f623b0ea..25dfb04f40 100644 --- a/internal/server/config.go +++ b/internal/server/config.go @@ -75,6 +75,8 @@ type ServerConfig struct { AllowedHosts []string // UserAgentMetadata specifies additional metadata to append to the User-Agent string. UserAgentMetadata []string + // PollInterval sets the polling frequency for configuration file updates. + PollInterval int } type logFormat string