-
Notifications
You must be signed in to change notification settings - Fork 0
feat(info): ten INFO fields from real sources, three waived with reasons (EC9) #504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -188,6 +188,100 @@ fn total_commands_sum() -> u64 { | |
| .sum() | ||
| } | ||
|
|
||
| // ── EC9: keyspace-change counter behind `rdb_changes_since_last_save` ──── | ||
| // The "is a save worth doing" signal every backup script reads. Sharded over | ||
| // the same padded per-thread slots as the total-commands counter for the same | ||
| // reason (a single line would bounce across every shard core at write rate); | ||
| // the increment is one relaxed fetch_add on the thread's own line. | ||
| // | ||
| // Counted at the storage funnels (`Database::set` / `remove` / `get_mut` / | ||
| // `clear` / `set_expiry`), not at dispatch: dispatch does not know whether a | ||
| // command mutated, and a phf flags lookup on the hot path is exactly the cost | ||
| // this codebase's perf invariants forbid. `get_mut` hands out mutable access | ||
| // that the caller may or may not use, so the count can run slightly HIGH. | ||
| // That direction is deliberate: over-counting says "changes pending" and | ||
| // triggers a save that was not needed, while under-counting would tell a | ||
| // backup script the dataset was clean when it was not. | ||
| static KEYSPACE_CHANGE_COUNTERS: [PaddedCounter; COMMAND_COUNTER_SLOTS] = | ||
| [PADDED_COUNTER_ZERO; COMMAND_COUNTER_SLOTS]; | ||
| /// Value of the change counter when the last save completed. `rdb_changes_ | ||
| /// since_last_save` is the difference; a save that completes concurrently with | ||
| /// writes can only make the difference smaller, never negative (saturating). | ||
| static KEYSPACE_CHANGES_AT_LAST_SAVE: AtomicU64 = AtomicU64::new(0); | ||
|
|
||
| /// Record one keyspace mutation. Hot path: one relaxed add, no allocation. | ||
| #[inline] | ||
| pub fn record_keyspace_change() { | ||
| COMMAND_COUNTER_SLOT.with(|&slot| { | ||
| KEYSPACE_CHANGE_COUNTERS[slot] | ||
| .0 | ||
| .fetch_add(1, Ordering::Relaxed); | ||
| }); | ||
| } | ||
|
|
||
| /// Exact sum across all slots. Read paths only (INFO). | ||
| fn keyspace_changes_sum() -> u64 { | ||
| KEYSPACE_CHANGE_COUNTERS | ||
| .iter() | ||
| .map(|c| c.0.load(Ordering::Relaxed)) | ||
| .sum() | ||
| } | ||
|
|
||
| /// Keyspace mutations since the last completed save (INFO `rdb_changes_since_last_save`). | ||
| pub fn rdb_changes_since_last_save() -> u64 { | ||
| keyspace_changes_sum().saturating_sub(KEYSPACE_CHANGES_AT_LAST_SAVE.load(Ordering::Relaxed)) | ||
| } | ||
|
|
||
| /// Mark a save as complete: subsequent changes count from here. | ||
| /// | ||
| /// Called on SAVE / BGSAVE success, never on failure — a failed save left the | ||
| /// dataset unpersisted, so the pending-change count must survive it. | ||
| pub fn mark_save_completed() { | ||
| KEYSPACE_CHANGES_AT_LAST_SAVE.store(keyspace_changes_sum(), Ordering::Relaxed); | ||
| } | ||
|
Comment on lines
+235
to
+241
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| // ── EC9: replica sync counters (INFO `sync_full` / `sync_partial_*`) ───── | ||
| // How an operator sees replicas thrashing: a climbing `sync_full` against a | ||
| // flat `sync_partial_ok` means partial resync keeps failing and every replica | ||
| // reconnect is re-shipping the whole dataset. Plain atomics — these fire once | ||
| // per replica handshake, not per command. | ||
| static SYNC_FULL: AtomicU64 = AtomicU64::new(0); | ||
| static SYNC_PARTIAL_OK: AtomicU64 = AtomicU64::new(0); | ||
| static SYNC_PARTIAL_ERR: AtomicU64 = AtomicU64::new(0); | ||
|
|
||
| /// A replica was served a full resync (`+FULLRESYNC`). | ||
| #[inline] | ||
| pub fn record_sync_full() { | ||
| SYNC_FULL.fetch_add(1, Ordering::Relaxed); | ||
| } | ||
|
|
||
| /// A replica's `PSYNC <id> <offset>` was satisfied from the backlog. | ||
| #[inline] | ||
| pub fn record_sync_partial_ok() { | ||
| SYNC_PARTIAL_OK.fetch_add(1, Ordering::Relaxed); | ||
| } | ||
|
|
||
| /// A replica asked for a partial resync that could not be served. | ||
| #[inline] | ||
| pub fn record_sync_partial_err() { | ||
| SYNC_PARTIAL_ERR.fetch_add(1, Ordering::Relaxed); | ||
| } | ||
|
|
||
| /// Full resyncs served since start. | ||
| pub fn sync_full() -> u64 { | ||
| SYNC_FULL.load(Ordering::Relaxed) | ||
| } | ||
|
|
||
| /// Partial resyncs served from the backlog since start. | ||
| pub fn sync_partial_ok() -> u64 { | ||
| SYNC_PARTIAL_OK.load(Ordering::Relaxed) | ||
| } | ||
|
|
||
| /// Partial-resync requests that had to fall back to a full resync. | ||
| pub fn sync_partial_err() -> u64 { | ||
| SYNC_PARTIAL_ERR.load(Ordering::Relaxed) | ||
| } | ||
|
|
||
| // ── P6: WAL aggressive reclamation counters (read by P10 INFO emitter) ─── | ||
| // Incremented by WalWriterV3::recycle_aggressive(). P10 reads these via the | ||
| // public getters below to populate the `# Reclamation` INFO section. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Parse the INFO manifest before server startup.
Line 655 validates waivers only after Redis and Moon start. If a binary is unavailable or a server does not become ready, an invalid waiver reports an infrastructure error instead of
ERR_UNREASONED_WAIVER.Parse
cfg.info_manifestduring the preflight phase inrun(). Pass the parsed fields into_info_coverage(). Add a unit test that confirms an unreasoned waiver does not call_spawn().Proposed change
def run(self) -> Report: cfg = self.cfg - entries = load_manifest(cfg.manifest_path) + entries = load_manifest(cfg.manifest_path) + info_fields = ( + self._parse_info_manifest(cfg.info_manifest) + if cfg.info_manifest else [] + ) if cfg.name_filter: entries = [e for e in entries if cfg.name_filter in e.name] ... if cfg.info_manifest: - results.extend(self._info_coverage(rport, mport)) + results.extend(self._info_coverage(rport, mport, info_fields))🤖 Prompt for AI Agents