Skip to content

fix(core): clear changedKeys at start of config reload to prevent stale notifications#6756

Merged
bluestreak01 merged 1 commit intomasterfrom
vi_file
Feb 9, 2026
Merged

fix(core): clear changedKeys at start of config reload to prevent stale notifications#6756
bluestreak01 merged 1 commit intomasterfrom
vi_file

Conversation

@bluestreak01
Copy link
Copy Markdown
Member

Summary

  • DynamicPropServerConfiguration.reload() accumulates stale entries in changedKeys across reload cycles when only a secret file changes (properties file unchanged), causing watchers to be spuriously notified about properties that did not change.
  • Root cause: changedKeys.clear() only lived inside updateSupportedProperties(), which is skipped when the properties file hasn't changed. Secret-file-only reloads added keys on top of the stale set.
  • Fix: move changedKeys.clear() to the top of reload(), inside the synchronized block, so every reload cycle starts fresh.

Test plan

  • New test testSecretFileRotationDoesNotNotifyStaleWatchers: registers a listener on a dynamic property, reloads after changing that property (listener fires), then rotates only a secret file and reloads again — asserts the listener does not fire a second time.
  • Full DynamicPropServerConfigurationTest suite passes (40 tests).

🤖 Generated with Claude Code

…le notifications

When reload() was called and only a secret file had changed (properties
file unchanged), updateSupportedProperties() was skipped—and with it the
only call to changedKeys.clear(). The set therefore accumulated keys from
prior reloads, causing watchers to be notified about properties that had
not actually changed.

Move the clear to the top of reload() so every cycle starts with a fresh
set regardless of which code path is taken.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 8, 2026

Walkthrough

The PR modifies DynamicPropServerConfiguration.reload() to clear the changedKeys set at the start of the synchronized block before loading new properties, ensuring only actual configuration changes from the reload are tracked. A test is added to verify that secret file rotation does not trigger listeners for unrelated watched properties.

Changes

Cohort / File(s) Summary
Core configuration tracking
core/src/main/java/io/questdb/DynamicPropServerConfiguration.java
Added explicit clearing of changedKeys set at the start of reload() to reset tracked changes before loading new properties.
Configuration reload tests
core/src/test/java/io/questdb/test/DynamicPropServerConfigurationTest.java
Added test testSecretFileRotationDoesNotNotifyStaleWatchers() validating that secret file rotation does not trigger listeners observing unrelated properties.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • questdb/questdb#6719: Directly related—implements the secret-file reload handling in DynamicPropServerConfiguration that this PR extends by clearing changedKeys and validates with the new test.
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: clearing changedKeys at the start of config reload to prevent stale notifications.
Description check ✅ Passed The description clearly explains the problem, root cause, and fix, with a comprehensive test plan that addresses the issue.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch vi_file

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@glasstiger
Copy link
Copy Markdown
Contributor

[PR Coverage check]

😍 pass : 1 / 1 (100.00%)

file detail

path covered line new line coverage
🔵 io/questdb/DynamicPropServerConfiguration.java 1 1 100.00%

@bluestreak01
Copy link
Copy Markdown
Member Author

@CodeRabbit review critically

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 8, 2026

🧩 Analysis chain

I'll review this PR critically. Let me gather the implementation details first.


🏁 Script executed:

#!/bin/bash
# Get the PR diff to see the exact changes
gh pr diff 6756 --repo questdb/questdb

Length of output: 4246


🏁 Script executed:

#!/bin/bash
# Get the full file content of the modified class
fd -t f "DynamicPropServerConfiguration.java" --exec cat {}

Length of output: 23198


🏁 Script executed:

#!/bin/bash
# Get the test file content
fd -t f "DynamicPropServerConfigurationTest.java" --exec cat {}

Length of output: 67875


🏁 Script executed:

#!/bin/bash
# Look for other usages of changedKeys to understand the full context
rg -n "changedKeys" --type java -C 5

Length of output: 19621


@bluestreak01 Here's my critical analysis of this fix:

Critical Issues

1. Breaking Change to Public Static Method Contract (Line 229)

The updateSupportedProperties() method is marked "// used in ent" and previously cleared changedKeys internally. Now callers must clear it themselves. This is an undocumented breaking change. If enterprise code calls this method directly, it will accumulate stale keys unless updated. Why wasn't this mentioned in the PR description or test plan?

Recommendation: Either document this as a breaking change for enterprise code, or add a parameter to control clearing behavior.

2. Inconsistent State on Exception (Lines 451-454)

If checkSecretFileChanges() throws CairoException after partially populating changedKeys (line 524), we abort the reload but leave changedKeys in a partially-mutated state. While it gets cleared on the next reload cycle, this makes the class's invariants fragile and error-prone.

Question: Should we wrap the entire mutation block in try-finally and clear changedKeys on any exception path?

3. Incomplete Test Coverage

The test testSecretFileRotationDoesNotNotifyStaleWatchers has gaps:

a) Missing verification: The test never confirms the password actually changed to "rotated_password". It only checks the listener counter. How do we know the reload worked at all?

// Missing after line 1486:
Assert.assertEquals("rotated_password", 
    serverMain.getConfiguration().getPGWireConfiguration().getDefaultPassword());

b) Scenario not tested: Secret-file-only reload as the first reload (no prior property changes). The bug could still exist in that case if changedKeys starts non-empty from previous test pollution.

c) Scenario not tested: Rotating a secret file for a property that IS being watched (e.g., watching pg.password, then rotating pg.password.file). Should that notify the watcher?

4. Lock Held During User Callbacks (Lines 460, 117-121)

The reload() method calls watchRegistry.notifyWatchers(changedKeys) while holding reloadLock, and notifyWatchers() is synchronized and directly invokes listener.configChanged(). If a listener callback is slow or attempts to call back into the configuration system, you risk deadlock or blocking all reloads.

Design question: Should notifications happen asynchronously or outside the synchronized block?

5. Edge Case: IOException Before Any Mutation (Lines 436-440)

If loading newProperties throws IOException at line 437, we return false at line 440 after having cleared changedKeys at line 433. This means:

  • Previous reload's changed keys are lost
  • We've mutated state before validating the operation can succeed

Is this acceptable behavior? It violates atomicity—either reload succeeds fully or leaves state unchanged.

Minor Issues

6. Memory Accumulation

The changedKeys ObjHashSet is cleared but never shrunk. After reloads with many changed keys followed by reloads with few, the set retains unnecessary capacity. Negligible in practice, but sloppy.

Proposed Test Enhancement

`@Test`
public void testSecretFileOnlyReloadAsFirstReload() throws Exception {
    // Test secret rotation when NO prior property changes occurred
    Path secretFilePath = Paths.get(temp.getRoot().getAbsolutePath(), "secrets", "pg_password.txt");
    Files.createDirectories(secretFilePath.getParent());
    Files.writeString(secretFilePath, "initial_password");
    
    try (FileWriter w = new FileWriter(serverConf)) {
        w.write("pg.password.file=" + secretFilePath.toString().replace("\\", "\\\\") + "\n");
    }
    
    try (ServerMain serverMain = new ServerMain(getBootstrap())) {
        serverMain.start();
        
        final AtomicLong listenerCounter = new AtomicLong(0);
        // Register listener for a property that's NOT changing
        serverMain.getEngine().getConfigReloader().watch(new ConfigReloader.Listener() {
            `@Override`
            public void configChanged() { listenerCounter.incrementAndGet(); }
            
            `@Override`
            public `@NotNull` ConfigPropertyKey[] getWatchedConfigKeys() {
                return new ConfigPropertyKey[]{ PropertyKey.CAIRO_SQL_ASOF_JOIN_EVACUATION_THRESHOLD };
            }
        });
        
        // Rotate secret file - listener should NOT fire since evacuation.threshold unchanged
        Files.writeString(secretFilePath, "rotated_password");
        serverMain.getEngine().getConfigReloader().reload();
        
        Assert.assertEquals(0, listenerCounter.get());
        // Verify password actually changed!
        Assert.assertEquals("rotated_password", 
            serverMain.getConfiguration().getPGWireConfiguration().getDefaultPassword());
    }
}

Bottom Line

The fix addresses the immediate bug but introduces a breaking change and has fragile exception handling. The test is insufficient—it doesn't verify the secret rotation actually worked and misses edge cases. Bump this back for:

  1. Documentation of breaking change
  2. Exception safety review
  3. Enhanced test coverage
  4. Consideration of lock-hold-time during callbacks

🧠 Learnings used
Learnt from: javier
Repo: questdb/questdb PR: 0
File: :0-0
Timestamp: 2026-02-06T18:22:28.130Z
Learning: In questdb/questdb, for PRs requested by javier, provide line-cited, push-back style reviews with concrete follow-ups (tests, diffs, and performance considerations).

Learnt from: javier
Repo: questdb/questdb PR: 0
File: :0-0
Timestamp: 2026-02-06T18:22:28.130Z
Learning: For questdb/questdb, javier expects a rigorous, push-back style review; always include concrete line-cited findings and propose targeted tests/fixes when reviewing javier’s PRs.

Learnt from: jerrinot
Repo: questdb/questdb PR: 0
File: :0-0
Timestamp: 2026-02-04T11:18:36.492Z
Learning: For PRs by jerrinot, always use a thorough and critical review mode: question design decisions, challenge assumptions, scrutinize implementations for edge cases and potential issues, and avoid superficial validation. They prefer hard-core reviews that push back on the code.
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@core/src/test/java/io/questdb/test/DynamicPropServerConfigurationTest.java`:
- Around line 1443-1500: Move the test method
testSecretFileRotationDoesNotNotifyStaleWatchers so it is alphabetically ordered
with other test methods: place it between the existing
testSecretFileProcPathRejected and testSecretFileSysPathRejected methods in
DynamicPropServerConfigurationTest; ensure imports and surrounding
code/annotations remain unchanged and run tests to verify no ordering-related
issues.
🧹 Nitpick comments (1)
core/src/test/java/io/questdb/test/DynamicPropServerConfigurationTest.java (1)

1490-1497: Consider also asserting that the rotated secret was actually applied.

The test only checks the listener wasn't re-notified, but doesn't verify the password was updated to "rotated_password". Adding an assertion on the actual config value would strengthen confidence that the secret rotation itself succeeded while still not triggering the unrelated watcher.

💡 Suggested addition after line 1491
                 Files.writeString(secretFilePath, "rotated_password");
                 serverMain.getEngine().getConfigReloader().reload();
 
+                // Verify the rotated password was actually applied
+                Assert.assertEquals(
+                        "rotated_password",
+                        serverMain.getConfiguration().getPGWireConfiguration().getDefaultPassword()
+                );
+
                 Assert.assertEquals(
                         "listener for evacuation.threshold should not fire on secret file rotation",
                         1,
                         listenerCounter.get()
                 );

Comment on lines +1443 to +1500
@Test
public void testSecretFileRotationDoesNotNotifyStaleWatchers() throws Exception {
assertMemoryLeak(() -> {
// Create a secret file for pg.password
Path secretFilePath = Paths.get(temp.getRoot().getAbsolutePath(), "secrets", "pg_password.txt");
Files.createDirectories(secretFilePath.getParent());
Files.writeString(secretFilePath, "initial_password");

// Initial config: a dynamic property + a secret file reference
try (FileWriter w = new FileWriter(serverConf)) {
w.write("cairo.sql.asof.join.evacuation.threshold=100\n");
w.write("pg.password.file=" + secretFilePath.toString().replace("\\", "\\\\") + "\n");
}

try (ServerMain serverMain = new ServerMain(getBootstrap())) {
serverMain.start();

// Register a listener that watches ONLY the evacuation threshold
final AtomicLong listenerCounter = new AtomicLong(0);
final var listener = new ConfigReloader.Listener() {
private final ConfigPropertyKey[] WATCHED_PROPERTIES = new ConfigPropertyKey[]{
PropertyKey.CAIRO_SQL_ASOF_JOIN_EVACUATION_THRESHOLD
};

@Override
public void configChanged() {
listenerCounter.incrementAndGet();
}

@Override
public @NotNull ConfigPropertyKey[] getWatchedConfigKeys() {
return WATCHED_PROPERTIES;
}
};
serverMain.getEngine().getConfigReloader().watch(listener);

// [1] Change the evacuation threshold in server.conf → listener should fire
try (FileWriter w = new FileWriter(serverConf)) {
w.write("cairo.sql.asof.join.evacuation.threshold=200\n");
w.write("pg.password.file=" + secretFilePath.toString().replace("\\", "\\\\") + "\n");
}
serverMain.getEngine().getConfigReloader().reload();
Assert.assertEquals(1, listenerCounter.get());

// [2] Now rotate ONLY the secret file. Do NOT touch server.conf.
// The listener watches evacuation.threshold which hasn't changed,
// so it should NOT fire again.
Files.writeString(secretFilePath, "rotated_password");
serverMain.getEngine().getConfigReloader().reload();

Assert.assertEquals(
"listener for evacuation.threshold should not fire on secret file rotation",
1,
listenerCounter.get()
);
}
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Place this test method in alphabetical order among its siblings.

Per coding guidelines, methods should be sorted alphabetically. testSecretFileRotationDoesNotNotifyStaleWatchers should sit between testSecretFileProcPathRejected (line 1263) and testSecretFileSysPathRejected (line 1281), not after testUnknownPropertyAdditionIsIgnored.

As per coding guidelines, "Group Java class members by kind and sort alphabetically. When adding new methods or fields, insert them in the correct alphabetical position among existing members of the same kind."

🤖 Prompt for AI Agents
In `@core/src/test/java/io/questdb/test/DynamicPropServerConfigurationTest.java`
around lines 1443 - 1500, Move the test method
testSecretFileRotationDoesNotNotifyStaleWatchers so it is alphabetically ordered
with other test methods: place it between the existing
testSecretFileProcPathRejected and testSecretFileSysPathRejected methods in
DynamicPropServerConfigurationTest; ensure imports and surrounding
code/annotations remain unchanged and run tests to verify no ordering-related
issues.

@bluestreak01 bluestreak01 merged commit d60a5f6 into master Feb 9, 2026
44 checks passed
@bluestreak01 bluestreak01 deleted the vi_file branch February 9, 2026 00:14
maciulis pushed a commit to maciulis/questdb that referenced this pull request Feb 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants