You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
_pr_with_fresh_issues starts with copy.copy(pr), so downstream mutations to top-level PullRequest fields land on the returned snapshot, not on the object stored inside self._cache.
The mirror buckets do not have the equivalent treatment. _isolate_for_downstream starts with copy.copy(cached_eval), but that is only a shallow copy of the MinerEvaluation. As a result, the returned evaluation still shares the same mirror_merged_prs, mirror_open_prs, and mirror_closed_prs lists and the same ScoredMirrorPR instances with self._cache[uid].evaluation.
This matters because the cache fallback path returns the cached evaluation before final score finalization. gittensor/validator/oss_contributions/reward.py calls store_or_use_cached_evaluation(...), then calls finalize_miner_scores(...) on the same miner_evaluations dict:
finalize_miner_scores walks legacy and mirror PR lists together and mutates top-level PR scoring fields:
scoring.py:318-320: writes collateral_score on open_pull_requests + mirror_open_prs
scoring.py:347-350: writes open_pr_spam_multiplier, credibility_multiplier, and earned_score on merged_pull_requests + mirror_merged_prs
scoring.py:273: writes pioneer_rank
scoring.py:293-294: writes pioneer_dividend and updates earned_score
For legacy PRs, those mutations are isolated by the per-PR copy.copy(...) in _pr_with_fresh_issues. For mirror PRs, they currently mutate the ScoredMirrorPR objects stored in the cache itself. A later cache hit can therefore return a mirror PR that already contains scoring state from a previous fallback round.
#824 fixed the mirror file-blob memory leak by stripping ScoredMirrorPR.files at cache store time, but _isolate_for_downstream was not updated to isolate the mirror buckets when those buckets were added to _build_cache_entry.
Steps to Reproduce
Build a MinerEvaluation with one ScoredMirrorPR in mirror_merged_prs.
Call cache.store(evaluation).
Call first = cache.get(uid, hotkey, github_id).
Mutate first.mirror_merged_prs[0].earned_score = 999.0 to simulate a finalize_miner_scores write.
Call second = cache.get(uid, hotkey, github_id).
Observe that second.mirror_merged_prs[0].earned_score == 999.0.
Repeat the same shape of test against merged_pull_requests with a legacy PullRequest; the mutation does not persist because _pr_with_fresh_issues returns a fresh PullRequest object.
Expected Behavior
MinerEvaluationCache.get() should return mirror PR instances that are independent of the objects stored in self._cache[uid].evaluation.
finalize_miner_scores should be free to mutate returned ScoredMirrorPR scoring fields without contaminating the cached snapshot used by future fallback rounds.
Mirror PR behavior should match the existing legacy PR behavior: one fresh top-level PR object per returned PR.
Actual Behavior
MinerEvaluationCache.get() returns the same ScoredMirrorPR instances stored inside self._cache[uid].evaluation.
When the cache fallback path later runs finalize_miner_scores, it can write the following fields directly onto cached mirror PR objects:
collateral_score
open_pr_spam_multiplier
credibility_multiplier
earned_score
pioneer_rank
pioneer_dividend
Subsequent cache.get() calls can return those already-mutated mirror PR objects.
Environment
OS: any
Python version: project default, currently Python 3.12
This is sufficient for the current production bug because the downstream writers after a cache hit mutate top-level ScoredMirrorPR scoring fields, not nested MirrorPullRequest metadata.
Nested mirror metadata such as MirrorPullRequest, MirrorReviewSummary, MirrorLabel, and MirrorLinkedIssue does not need fresh copies for this bug. Current gittensor/ code after a cache hit reads those fields but does not mutate them. This differs from legacy PullRequest.issues, where Issue has mutable discovery_* fields and therefore _pr_with_fresh_issues also refreshes the issues list.
Suggested Tests
Add cache-isolation tests that verify:
A mutation to first.mirror_merged_prs[0].earned_score after cache.get() does not appear in a second cache.get().
A mutation to first.mirror_open_prs[0].collateral_score after cache.get() does not appear in a second cache.get().
Copying mirror_closed_prs is still recommended for symmetry and future safety, even though current finalization does not mutate closed mirror PR objects directly.
Description
MinerEvaluationCache._isolate_for_downstreamingittensor/classes.pyreturns an isolated snapshot for legacy PRs, but not for mirror PRs.For legacy PR buckets,
_isolate_for_downstreamrebuilds each list with_pr_with_fresh_issues(...):_pr_with_fresh_issuesstarts withcopy.copy(pr), so downstream mutations to top-levelPullRequestfields land on the returned snapshot, not on the object stored insideself._cache.The mirror buckets do not have the equivalent treatment.
_isolate_for_downstreamstarts withcopy.copy(cached_eval), but that is only a shallow copy of theMinerEvaluation. As a result, the returned evaluation still shares the samemirror_merged_prs,mirror_open_prs, andmirror_closed_prslists and the sameScoredMirrorPRinstances withself._cache[uid].evaluation.This matters because the cache fallback path returns the cached evaluation before final score finalization.
gittensor/validator/oss_contributions/reward.pycallsstore_or_use_cached_evaluation(...), then callsfinalize_miner_scores(...)on the sameminer_evaluationsdict:finalize_miner_scoreswalks legacy and mirror PR lists together and mutates top-level PR scoring fields:scoring.py:318-320: writescollateral_scoreonopen_pull_requests + mirror_open_prsscoring.py:347-350: writesopen_pr_spam_multiplier,credibility_multiplier, andearned_scoreonmerged_pull_requests + mirror_merged_prsscoring.py:273: writespioneer_rankscoring.py:293-294: writespioneer_dividendand updatesearned_scoreFor legacy PRs, those mutations are isolated by the per-PR
copy.copy(...)in_pr_with_fresh_issues. For mirror PRs, they currently mutate theScoredMirrorPRobjects stored in the cache itself. A later cache hit can therefore return a mirror PR that already contains scoring state from a previous fallback round.#824 fixed the mirror file-blob memory leak by stripping
ScoredMirrorPR.filesat cache store time, but_isolate_for_downstreamwas not updated to isolate the mirror buckets when those buckets were added to_build_cache_entry.Steps to Reproduce
MinerEvaluationwith oneScoredMirrorPRinmirror_merged_prs.cache.store(evaluation).first = cache.get(uid, hotkey, github_id).first.mirror_merged_prs[0].earned_score = 999.0to simulate afinalize_miner_scoreswrite.second = cache.get(uid, hotkey, github_id).second.mirror_merged_prs[0].earned_score == 999.0.merged_pull_requestswith a legacyPullRequest; the mutation does not persist because_pr_with_fresh_issuesreturns a freshPullRequestobject.Expected Behavior
MinerEvaluationCache.get()should return mirror PR instances that are independent of the objects stored inself._cache[uid].evaluation.finalize_miner_scoresshould be free to mutate returnedScoredMirrorPRscoring fields without contaminating the cached snapshot used by future fallback rounds.Mirror PR behavior should match the existing legacy PR behavior: one fresh top-level PR object per returned PR.
Actual Behavior
MinerEvaluationCache.get()returns the sameScoredMirrorPRinstances stored insideself._cache[uid].evaluation.When the cache fallback path later runs
finalize_miner_scores, it can write the following fields directly onto cached mirror PR objects:collateral_scoreopen_pr_spam_multipliercredibility_multiplierearned_scorepioneer_rankpioneer_dividendSubsequent
cache.get()calls can return those already-mutated mirror PR objects.Environment
test, post-fix: strip mirror file contents in MinerEvaluationCache #824Suggested Fix
Add mirror PR top-level copy isolation in
_isolate_for_downstream, mirroring the existing legacy bucket treatment:This is sufficient for the current production bug because the downstream writers after a cache hit mutate top-level
ScoredMirrorPRscoring fields, not nestedMirrorPullRequestmetadata.Nested mirror metadata such as
MirrorPullRequest,MirrorReviewSummary,MirrorLabel, andMirrorLinkedIssuedoes not need fresh copies for this bug. Currentgittensor/code after a cache hit reads those fields but does not mutate them. This differs from legacyPullRequest.issues, whereIssuehas mutablediscovery_*fields and therefore_pr_with_fresh_issuesalso refreshes theissueslist.Suggested Tests
Add cache-isolation tests that verify:
first.mirror_merged_prs[0].earned_scoreaftercache.get()does not appear in a secondcache.get().first.mirror_open_prs[0].collateral_scoreaftercache.get()does not appear in a secondcache.get().ScoredMirrorPR.filesis still stripped at store time, preserving the fix: strip mirror file contents in MinerEvaluationCache #824 memory fix.Copying
mirror_closed_prsis still recommended for symmetry and future safety, even though current finalization does not mutate closed mirror PR objects directly.