kad: add popularity decay so saturated m_filenames can still rotate - #799
Merged
Merged
Conversation
irwir's review on amule-project#795 caught that once CKeyEntry::m_filenames hits the 100-entry cap and every survivor has popularity >= 2, a fresh popularity-1 candidate can never break in. The cap stays -- it's the RAM lever that bounds the keyword index on busy publishing nodes (amule-project#314) -- but rotation needs an independent mechanism. Three pieces: 1. Per-CKeyEntry m_mergeCounter. Incremented on every real merge (fromEntry != NULL; the IP-init no-op path does not count). 2. Decay tick. Every MERGES_PER_DECAY_TICK real merges, decrement every entry's m_popularityIndex by 1 (floored at 0). Gated on m_filenames.size() >= MAX_FILENAMES: under the cap there's nothing to rotate, and decay would only drag popularity numbers down before the list is even full. Once saturated the list never shrinks below the cap (pushBounded only ever replaces), so the gate is "off until first saturation, then always on". 3. Zero-popularity escape hatch in pushBounded. If the weakest slot has decayed to popularity 0, any candidate (including a fresh popularity-1 entry) replaces it. Stagnant incumbents age out as their popularity decays away; genuinely-popular names hold position because matching publishes bump them faster than decay drops them. Plus a robustness fix in CEntry::GetCommonFileName. Before this commit, the loop initialised the running max with 0 and used a strict `>` comparison, so an all-zero m_filenames (which decay can produce on diffusely-published keys with no clear winner) would leave the result iterator at end() and return an empty string. That would silently break callers downstream: GetTagCount drops TAG_FILENAME from the count, WriteTagListInc omits it on the wire, SearchTermsMatch returns false for every term, and CIndexed::AddKeyword rejects the entry via the GetCommonFileName().IsEmpty() guard. Seeding the running max from the first entry (and switching to a "pick the first if all tied" fallback) preserves the protocol invariant "non-empty m_filenames yields a non-empty common name" without changing behaviour for any case where a real winner exists. Starting MERGES_PER_DECAY_TICK = 20 is a reasonable guess; tunable later if rotation half-lives in production turn out to need it. On-disk format unchanged -- m_mergeCounter is not serialised; the decay phase resets per key on restart, which just means a one-time grace period before decay resumes (first decay tick on any active key still fires within minutes of restart). Refs amule-project#765, amule-project#795.
2 tasks
got3nks
added a commit
to got3nks/amule
that referenced
this pull request
Jun 1, 2026
Reverts the decay tick + zero-popularity escape hatch landed in df10aa8 ("kad: add popularity decay so saturated m_filenames can still rotate") and addressed in PR amule-project#799. Discussion on amule-project#795 with @irwir after amule-project#799 landed surfaced the existing entry-lifetime cleanup that already handles the "publisher offline => entry invalidation" case the decay was targeting: - CKeyEntry::CleanUpTrackedPublishers ages out individual publisher IPs from m_publishingIPs after KADEMLIAREPUBLISHTIMEK (24 h) of silence. - CIndexed::Clean deletes the whole CKeyEntry when its m_tLifeTime expires (same 24 h horizon, extended on every publisher refresh). So "publisher went offline" is handled at both the per-IP and per-entry granularities. The only thing left uncleaned is a single name variant within an active CKeyEntry, which is exclusively a saturated-m_filenames concern. Empirical data from a live HighID production node (~4000 merges over ~4 h across ~1800 indexed keys) shows the saturated-key count never lifts off zero -- so the regime decay was designed for doesn't appear in practice on a healthy node. At the rare saturated cases the "freeze at the cap" behaviour the decay was overriding is now the correct behaviour: bounded RAM, no spurious rotation of legitimate vote winners when traffic dips. The 100-entry cap from amule-project#795 / amule-project#314 stays. It protects against a different case from the whole-entry TTL: an *active* CKeyEntry that adversarial or pathological publishers feed unbounded variant names under the same hash. As @irwir noted in this thread, "setting a limit would be reasonable to prevent attacks and misuse." The cap is a single integer comparison on insert and lines up with the m_publishingIPs cap right next to it. Concretely reverted: - CKeyEntry::MERGES_PER_DECAY_TICK static + m_mergeCounter member. - The decay tick block at the end of MergeIPsAndFilenames. - The "weakest->m_popularityIndex == 0" escape hatch in pushBounded -- back to the original strict `>` comparison from amule-project#795. Deliberately kept: the GetCommonFileName robustness fix from df10aa8. Our own code no longer produces popularity-0 entries after this revert, but two paths we don't control can still land them in m_filenames: on-disk data from any node that ran the decay build (it could have written 0-popularity entries to known2.met before this revert lands), and malformed/adversarial publishes that send popularity = 0 over the wire. The picker's seed-from-first / "pick first on tie" fallback closes a sharp edge in a function whose output is on the wire (TAG_FILENAME, SearchTermsMatch, CIndexed::AddKeyword reject path) for essentially no cost. Refs amule-project#795.
mrjimenez
pushed a commit
that referenced
this pull request
Jun 1, 2026
Reverts the decay tick + zero-popularity escape hatch landed in df10aa8 ("kad: add popularity decay so saturated m_filenames can still rotate") and addressed in PR #799. Discussion on #795 with @irwir after #799 landed surfaced the existing entry-lifetime cleanup that already handles the "publisher offline => entry invalidation" case the decay was targeting: - CKeyEntry::CleanUpTrackedPublishers ages out individual publisher IPs from m_publishingIPs after KADEMLIAREPUBLISHTIMEK (24 h) of silence. - CIndexed::Clean deletes the whole CKeyEntry when its m_tLifeTime expires (same 24 h horizon, extended on every publisher refresh). So "publisher went offline" is handled at both the per-IP and per-entry granularities. The only thing left uncleaned is a single name variant within an active CKeyEntry, which is exclusively a saturated-m_filenames concern. Empirical data from a live HighID production node (~4000 merges over ~4 h across ~1800 indexed keys) shows the saturated-key count never lifts off zero -- so the regime decay was designed for doesn't appear in practice on a healthy node. At the rare saturated cases the "freeze at the cap" behaviour the decay was overriding is now the correct behaviour: bounded RAM, no spurious rotation of legitimate vote winners when traffic dips. The 100-entry cap from #795 / #314 stays. It protects against a different case from the whole-entry TTL: an *active* CKeyEntry that adversarial or pathological publishers feed unbounded variant names under the same hash. As @irwir noted in this thread, "setting a limit would be reasonable to prevent attacks and misuse." The cap is a single integer comparison on insert and lines up with the m_publishingIPs cap right next to it. Concretely reverted: - CKeyEntry::MERGES_PER_DECAY_TICK static + m_mergeCounter member. - The decay tick block at the end of MergeIPsAndFilenames. - The "weakest->m_popularityIndex == 0" escape hatch in pushBounded -- back to the original strict `>` comparison from #795. Deliberately kept: the GetCommonFileName robustness fix from df10aa8. Our own code no longer produces popularity-0 entries after this revert, but two paths we don't control can still land them in m_filenames: on-disk data from any node that ran the decay build (it could have written 0-popularity entries to known2.met before this revert lands), and malformed/adversarial publishes that send popularity = 0 over the wire. The picker's seed-from-first / "pick first on tie" fallback closes a sharp edge in a function whose output is on the wire (TAG_FILENAME, SearchTermsMatch, CIndexed::AddKeyword reject path) for essentially no cost. Refs #795.
1 task
got3nks
added a commit
to got3nks/amule
that referenced
this pull request
Jun 4, 2026
…ndex Adds 55+ merged PRs to the 3.0.0 changelog since the last update (amule-project#747, 2026-05-27). Narrative additions cover: - Packaging: expanded the top list to include the macOS per-arch .app bundles and the Windows NSIS installer alongside the existing AppImage / Flatpak / .dmg / .zip entries. New bullets for amule-project#785 (alc/alcc/cas/wxcas everywhere + Windows amuleweb), amule-project#794 (.dmg amuleweb path), amule-project#789 (<OS>-<arch> artifact naming), amule-project#780 / amule-project#796 (Windows DPI + comctl32 manifest), amule-project#784 (FHS share/amule paths). - Bug Fixes & Stability: post-amule-project#744 fixes including EC notification leak (amule-project#797), big-library scaling (amule-project#736, amule-project#840 superseding amule-project#728), amulegui ghost entries (amule-project#810, amule-project#819, amule-project#841, amule-project#824, amule-project#830, amule-project#760), PartFile early hash (amule-project#762), server protocol fixes (amule-project#835, amule-project#788, amule-project#721, amule-project#787), crypto stream UB (amule-project#779), UAF prevention (amule-project#756), Kad rotation (amule-project#795, amule-project#799/amule-project#805), GTK warning silencing (amule-project#833, amule-project#826/amule-project#836), and the clang-tidy worklist (amule-project#770, amule-project#772-amule-project#774). - Translations: late-cycle wave covering French/Turkish manpages (amule-project#753/amule-project#754/amule-project#776), Galician (amule-project#763), Slovenian (amule-project#771), pt-BR (amule-project#768/amule-project#775/amule-project#812), French (amule-project#811), plus man-page tooling for date+version drift (amule-project#802). - Contributors: added ngosang for UX feedback on the late-3.0 cycle (amule-project#817/amule-project#818/amule-project#821/amule-project#828/amule-project#844) and ongoing work on the user-facing manual at amule-org.github.io. - Merged PRs flat index: extended with amule-project#746-amule-project#845 + amule-project#841.
got3nks
added a commit
to got3nks/amule
that referenced
this pull request
Jun 4, 2026
…ndex Adds 55+ merged PRs to the 3.0.0 changelog since the last update (amule-project#747, 2026-05-27). Narrative additions cover: - Packaging: expanded the top list to include the macOS per-arch .app bundles and the Windows NSIS installer alongside the existing AppImage / Flatpak / .dmg / .zip entries. New bullets for amule-project#785 (alc/alcc/cas/wxcas everywhere + Windows amuleweb), amule-project#794 (.dmg amuleweb path), amule-project#789 (<OS>-<arch> artifact naming), amule-project#780 / amule-project#796 (Windows DPI + comctl32 manifest), amule-project#784 (FHS share/amule paths). - Bug Fixes & Stability: post-amule-project#744 fixes including EC notification leak (amule-project#797), big-library scaling (amule-project#736, amule-project#840 superseding amule-project#728), amulegui ghost entries (amule-project#810, amule-project#819, amule-project#841, amule-project#824, amule-project#830, amule-project#760), PartFile early hash (amule-project#762), server protocol fixes (amule-project#835, amule-project#788, amule-project#721, amule-project#787), crypto stream UB (amule-project#779), UAF prevention (amule-project#756), Kad rotation (amule-project#795, amule-project#799/amule-project#805), GTK warning silencing (amule-project#833, amule-project#826/amule-project#836), and the clang-tidy worklist (amule-project#770, amule-project#772-amule-project#774). - Translations: late-cycle wave covering French/Turkish manpages (amule-project#753/amule-project#754/amule-project#776), Galician (amule-project#763), Slovenian (amule-project#771), pt-BR (amule-project#768/amule-project#775/amule-project#812), French (amule-project#811), plus man-page tooling for date+version drift (amule-project#802). - Contributors: added ngosang for UX feedback on the late-3.0 cycle (amule-project#817/amule-project#818/amule-project#821/amule-project#828/amule-project#844) and ongoing work on the user-facing manual at amule-org.github.io. - Merged PRs flat index: extended with amule-project#746-amule-project#845 + amule-project#841.
mrjimenez
pushed a commit
that referenced
this pull request
Jun 4, 2026
Adds 55+ merged PRs to the 3.0.0 changelog since the last update (#747, 2026-05-27). Narrative additions cover: - Packaging: expanded the top list to include the macOS per-arch .app bundles and the Windows NSIS installer alongside the existing AppImage / Flatpak / .dmg / .zip entries. New bullets for #785 (alc/alcc/cas/wxcas everywhere + Windows amuleweb), #794 (.dmg amuleweb path), #789 (<OS>-<arch> artifact naming), #780 / #796 (Windows DPI + comctl32 manifest), #784 (FHS share/amule paths). - Bug Fixes & Stability: post-#744 fixes including EC notification leak (#797), big-library scaling (#736, #840 superseding #728), amulegui ghost entries (#810, #819, #841, #824, #830, #760), PartFile early hash (#762), server protocol fixes (#835, #788, #721, #787), crypto stream UB (#779), UAF prevention (#756), Kad rotation (#795, #799/#805), GTK warning silencing (#833, #826/#836), and the clang-tidy worklist (#770, #772-#774). - Translations: late-cycle wave covering French/Turkish manpages (#753/#754/#776), Galician (#763), Slovenian (#771), pt-BR (#768/#775/#812), French (#811), plus man-page tooling for date+version drift (#802). - Contributors: added ngosang for UX feedback on the late-3.0 cycle (#817/#818/#821/#828/#844) and ongoing work on the user-facing manual at amule-org.github.io. - Merged PRs flat index: extended with #746-#845 + #841.
mrjimenez
pushed a commit
to mrjimenez/amule
that referenced
this pull request
Aug 5, 2026
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 7. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](actions/setup-node@v4...v7) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #795 addressing irwir's review.
Problem
Once
CKeyEntry::m_filenamessaturates at the 100-entry cap and every survivor has popularity ≥ 2, a fresh popularity-1 candidate can never break in.pushBounded's strict>drops them on the floor. Pre-existing behaviour from #314 (the originalsort + resizehad the same algorithmic semantic); #795 just made it visible. Cap stays — it's the RAM lever that bounds the keyword index — so rotation needs an independent mechanism.Three pieces
Per-
CKeyEntrymerge counter.m_mergeCounter(uint32_t) increments on every real merge (fromEntry != NULL; the IP-init no-op path doesn't count).Decay tick. Every
MERGES_PER_DECAY_TICK(= 20) real merges, every entry'sm_popularityIndexis decremented by 1 (floored at 0). Gated onm_filenames.size() >= MAX_FILENAMES: under the cap there's nothing to rotate, and decay would just drag popularity numbers down before the list is even full. Once saturated the list never shrinks (pushBoundedonly ever replaces in place), so the gate is "off until first saturation, then always on" for the lifetime of the entry.Zero-popularity escape hatch in
pushBounded. If the weakest slot has decayed to 0, any candidate replaces it. Stagnant incumbents age out as their popularity decays; popular names hold position because matching publishes bump them faster than decay drops them. One-shot variants drift to 0 within a few dozen publish cycles to the same hash.Robustness fix in
GetCommonFileNameThe original loop initialised the running max to 0 and used a strict
> highestPopularityIndexcomparison, so an all-zerom_filenames(which decay can produce on diffusely-published keys with no clear winner) would leave the result iterator atend()and return an empty string. Silently breaks every downstream consumer:GetTagCountdropsTAG_FILENAMEfrom the countWriteTagListIncomits the name on the wireSearchTermsMatchreturnsfalsefor every search termCIndexed::AddKeywordrejects the entry via theGetCommonFileName().IsEmpty()guardSeed the running max from the first entry (and switch to a "pick the first if all tied" fallback) — preserves the protocol invariant "non-empty
m_filenamesyields a non-empty common name" without changing behaviour for any case where a real winner exists.Tuning
MERGES_PER_DECAY_TICK = 20is a reasonable starter. Observed on a real publishing-heavy production node: ~12.8 merges/min across ~1800 indexed keys → power-law-weighted hot keys see decay every ~20 min to ~7 h depending on traffic. Cold keys never saturate, so the gate skips them.On-disk format
Unchanged.
m_mergeCounteris not serialised; decay phase resets per key on restart, which just means a one-time grace period before decay resumes — first decay tick on any active key still fires within minutes of restart.Refs #765, #795.