Match the folder-watch reconcile to the legacy watched-folder poll

This commit is contained in:
Anthony Stirling
2026-08-13 11:56:02 +01:00
parent d679ee9971
commit 2945d5149c
4 changed files with 74 additions and 3 deletions
@@ -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) {
@@ -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
@@ -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.
@@ -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