Skip to content

Commit cc39991

Browse files
fix(resolver): exempt top-level pinned versions from transitive cooldown
Add `exempt_versions` field to `Cooldown` so that only the specific version(s) approved via a top-level `==` pin bypass the cooldown age check when resolved as transitive dependencies. Other versions remain subject to cooldown filtering. Fixes: #1153 Co-Authored-By: Claude <claude@anthropic.com> Signed-off-by: Lalatendu Mohanty <lmohanty@redhat.com>
1 parent bd86c56 commit cc39991

3 files changed

Lines changed: 349 additions & 28 deletions

File tree

src/fromager/candidate.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ class Cooldown:
2121
2222
bootstrap_time is fixed at construction so all resolutions in a single run
2323
share the same cutoff.
24+
25+
exempt_versions bypasses the age check for specific versions that were
26+
already approved via a top-level exact pin.
2427
"""
2528

2629
min_age: datetime.timedelta
2730
bootstrap_time: datetime.datetime = dataclasses.field(
2831
default_factory=lambda: datetime.datetime.now(datetime.UTC)
2932
)
33+
exempt_versions: frozenset[Version] = dataclasses.field(default_factory=frozenset)
3034

3135

3236
@dataclasses.dataclass(frozen=True, order=True, slots=True, repr=False, kw_only=True)

src/fromager/resolver.py

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -147,42 +147,89 @@ def _has_equality_pin(req: Requirement) -> bool:
147147
return len(specs) == 1 and specs[0].operator == "==" and "*" not in specs[0].version
148148

149149

150+
def _get_toplevel_pinned_versions(
151+
ctx: context.WorkContext, req: Requirement
152+
) -> frozenset[Version]:
153+
"""Return versions of *req* that have a top-level exact ``==`` pin in the graph."""
154+
top_level_edges = ctx.dependency_graph.get_root_node().get_outgoing_edges(
155+
req.name, RequirementType.TOP_LEVEL
156+
)
157+
return frozenset(
158+
edge.destination_node.version
159+
for edge in top_level_edges
160+
if _has_equality_pin(edge.req)
161+
)
162+
163+
164+
def _resolve_cooldown_params(
165+
ctx: context.WorkContext,
166+
req: Requirement,
167+
) -> tuple[datetime.timedelta, datetime.datetime] | None:
168+
"""Resolve min_age and bootstrap_time for a package's cooldown.
169+
170+
Returns ``None`` when cooldown is disabled (no global/per-package
171+
configuration, or a per-package override of 0). Otherwise returns
172+
the effective ``(min_age, bootstrap_time)`` pair.
173+
"""
174+
min_age_override = ctx.package_build_info(req).resolver_min_release_age
175+
176+
if ctx.cooldown is None and min_age_override is None:
177+
return None
178+
if min_age_override == 0:
179+
return None
180+
181+
if min_age_override is not None:
182+
min_age = datetime.timedelta(days=min_age_override)
183+
elif ctx.cooldown is not None:
184+
min_age = ctx.cooldown.min_age
185+
else:
186+
return None
187+
188+
bootstrap_time = (
189+
ctx.cooldown.bootstrap_time
190+
if ctx.cooldown is not None
191+
else datetime.datetime.now(datetime.UTC)
192+
)
193+
return min_age, bootstrap_time
194+
195+
150196
def resolve_package_cooldown(
151197
ctx: context.WorkContext,
152198
req: Requirement,
153199
req_type: RequirementType | None = None,
154200
) -> Cooldown | None:
155201
"""Compute the effective cooldown for a single package.
156202
157-
Args:
158-
ctx: The current work context (provides the global cooldown).
159-
req: The package requirement being resolved.
160-
req_type: The requirement type (top-level, install, etc.).
203+
Returns ``None`` (cooldown disabled) when:
204+
205+
* The requirement is a top-level exact ``==`` pin — the user explicitly
206+
approved that version.
207+
* A per-package ``min_release_age=0`` override disables cooldown.
208+
* No global cooldown is configured and no per-package override enables one.
161209
162-
Returns:
163-
The cooldown to pass to the provider, or ``None`` if disabled.
210+
Otherwise a ``Cooldown`` is returned with:
211+
212+
* *min_age* from the per-package override (if set) or the global cooldown.
213+
* *bootstrap_time* inherited from the global cooldown (for a consistent
214+
cutoff across the entire run) or the current time.
215+
* *exempt_versions* populated from top-level exact-pinned entries in the
216+
dependency graph, so transitive resolutions of the same package honour
217+
the user's explicit pin.
164218
"""
165219
if req_type == RequirementType.TOP_LEVEL and _has_equality_pin(req):
166220
if ctx.cooldown is not None:
167221
logger.info("cooldown bypassed as the top-level requirement uses == pin")
168222
return None
169223

170-
per_package_days = ctx.package_build_info(req).resolver_min_release_age
171-
global_cooldown = ctx.cooldown
172-
if per_package_days is None:
173-
return global_cooldown
174-
if per_package_days == 0:
224+
params = _resolve_cooldown_params(ctx, req)
225+
if params is None:
175226
return None
176-
# Per-package positive override: inherit bootstrap_time from global so all
177-
# resolutions in a single run share the same fixed cutoff point.
178-
bootstrap_time = (
179-
global_cooldown.bootstrap_time
180-
if global_cooldown is not None
181-
else datetime.datetime.now(datetime.UTC)
182-
)
227+
228+
min_age, bootstrap_time = params
183229
return Cooldown(
184-
min_age=datetime.timedelta(days=per_package_days),
230+
min_age=min_age,
185231
bootstrap_time=bootstrap_time,
232+
exempt_versions=_get_toplevel_pinned_versions(ctx, req),
186233
)
187234

188235

@@ -685,11 +732,12 @@ def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> boo
685732
def is_blocked_by_cooldown(self, candidate: Candidate) -> bool:
686733
"""Return True if the candidate is rejected by the release-age cooldown."""
687734

688-
# a cooldown is not specified...
689735
if self.cooldown is None:
690736
return False
691737

692-
# the target candidate doesn't provide a valid upload timestamp
738+
if candidate.version in self.cooldown.exempt_versions:
739+
return False
740+
693741
if candidate.upload_time is None:
694742
if not self.supports_upload_time:
695743
# this provider does not yet support timestamp retrieval (e.g. GitHub).

0 commit comments

Comments
 (0)