diff --git a/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java b/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java index 6d3f366b8e..3e5a8aab65 100644 --- a/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java +++ b/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java @@ -24,6 +24,11 @@ public class ConfigInitializer { private static final int MIN_SETTINGS_FILE_LINES = 31; + /** The shipped default before folder-watch had to cover converted legacy watched folders. */ + private static final int OLD_WATCH_RECONCILE_SECONDS = 300; + + private static final int WATCH_RECONCILE_SECONDS = 60; + public void ensureConfigExists() throws IOException, URISyntaxException { // 1) If settings file doesn't exist, create from template Path destPath = Path.of(InstallationPathConfig.getSettingsPath()); @@ -80,6 +85,7 @@ public class ConfigInitializer { migrateEnterpriseEditionToPremium(settingsFile, settingsTemplateFile); migrateProFeaturesKeyCasing(settingsFile, settingsTemplateFile); + migrateWatchReconcileDefault(settingsFile); boolean changesMade = settingsTemplateFile.updateValuesFromYaml(settingsFile, settingsTemplateFile); @@ -102,6 +108,29 @@ public class ConfigInitializer { } } + // TODO: Remove post migration + // A legacy watched folder polled every 60s, and converting one into a folder-watch policy must + // not make it slower. The reconcile is the only retry for a file the readiness check deferred, + // so at 300s a file that landed mid-sweep waited five minutes. Existing installs carry the old + // default forward on upgrade, so lower it here - but only when it is still exactly the old + // default, since any other value is a deliberate choice. + void migrateWatchReconcileDefault(YamlHelper yaml) { + // getValueByExactKeyPath yields the scalar as written, i.e. a String, not a Number. + Object current = yaml.getValueByExactKeyPath("policies", "watchReconcileSeconds"); + if (current == null + || !String.valueOf(current) + .trim() + .equals(String.valueOf(OLD_WATCH_RECONCILE_SECONDS))) { + return; + } + yaml.updateValue(List.of("policies", "watchReconcileSeconds"), WATCH_RECONCILE_SECONDS); + log.info( + "Lowered policies.watchReconcileSeconds from {} to {} so converted watched folders" + + " keep their legacy poll interval", + OLD_WATCH_RECONCILE_SECONDS, + WATCH_RECONCILE_SECONDS); + } + // TODO: Remove post migration private void migrateEnterpriseEditionToPremium(YamlHelper yaml, YamlHelper template) { if (yaml.getValueByExactKeyPath("enterpriseEdition", "enabled") != null) { diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index a8bc2812b7..1e0cb3d462 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -221,9 +221,10 @@ public class ApplicationProperties { /** * How often (seconds) the folder-watch trigger reconciles its watch registrations and * re-runs every folder-watch policy as a safety net for filesystem events that were missed - * (NFS, bind mounts, inotify-queue overflow). + * (NFS, bind mounts, inotify-queue overflow). Matches the legacy watched-folder scanner's + * poll, so a converted automation picks a file up no slower than it used to. */ - private long watchReconcileSeconds = 300; + private long watchReconcileSeconds = 60; /** * How long (milliseconds) the folder-watch trigger keeps draining filesystem events after diff --git a/app/common/src/test/java/stirling/software/common/configuration/ConfigInitializerTest.java b/app/common/src/test/java/stirling/software/common/configuration/ConfigInitializerTest.java index 4ce75d0c62..c46f6612d7 100644 --- a/app/common/src/test/java/stirling/software/common/configuration/ConfigInitializerTest.java +++ b/app/common/src/test/java/stirling/software/common/configuration/ConfigInitializerTest.java @@ -30,6 +30,47 @@ class ConfigInitializerTest { producer: Stirling-PDF """; + @Test + void migrateWatchReconcileDefault_lowersTheOldShippedDefault() { + YamlHelper existing = + new YamlHelper( + LOAD_SETTINGS, + """ + policies: + scheduleSweepSeconds: 60 + watchReconcileSeconds: 300 + """); + + new ConfigInitializer().migrateWatchReconcileDefault(existing); + + assertEquals("60", existing.getValueByExactKeyPath("policies", "watchReconcileSeconds")); + // Untouched neighbours prove the rewrite is scoped to the one key. + assertEquals("60", existing.getValueByExactKeyPath("policies", "scheduleSweepSeconds")); + } + + @Test + void migrateWatchReconcileDefault_keepsADeliberateValue() { + YamlHelper existing = + new YamlHelper( + LOAD_SETTINGS, + """ + policies: + watchReconcileSeconds: 900 + """); + + new ConfigInitializer().migrateWatchReconcileDefault(existing); + + assertEquals("900", existing.getValueByExactKeyPath("policies", "watchReconcileSeconds")); + } + + @Test + void migrateWatchReconcileDefault_toleratesAMissingKey() { + YamlHelper existing = + new YamlHelper(LOAD_SETTINGS, "policies:\n scheduleSweepSeconds: 60\n"); + + assertDoesNotThrow(() -> new ConfigInitializer().migrateWatchReconcileDefault(existing)); + } + @Test void migrateProFeaturesKeyCasing_carriesForwardLegacyPascalCaseValues() { // An existing install whose settings.yml still uses the old PascalCase keys. diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index d0234a3594..e639dfdad2 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -415,7 +415,7 @@ policies: # disabled in SaaS mode. allowedFolderRoots: [] # e.g. ["/data/inbox", "/data/outbox"] scheduleSweepSeconds: 60 # How often (seconds) scheduled policies are checked for being due - watchReconcileSeconds: 300 # How often (seconds) folder-watch re-syncs watches and re-runs as a safety net for missed events + watchReconcileSeconds: 60 # How often (seconds) folder-watch re-syncs watches and re-runs as a safety net for missed events watchQuietPeriodMs: 500 # How long (ms) folder-watch coalesces a burst of file events into a single run streamTimeoutMs: 1800000 # SSE timeout (ms) for live run-progress streams runExpiryMinutes: 30 # How long (minutes) a finished run's in-memory state is kept before eviction