Skip to content

fix(sessions)!: make session retirement permanent - #2348

Open
gauravch-code wants to merge 2 commits into
apify:v2from
gauravch-code:fix/2312-terminal-session-retirement
Open

gauravch-code wants to merge 2 commits into
apify:v2from
gauravch-code:fix/2312-terminal-session-retirement

Conversation

@gauravch-code

Copy link
Copy Markdown

Description

Keep retired sessions from becoming usable again when mark_good() lowers their error score. A session explicitly retired after a blocked request should not be handed out again by the pool, including after saving and restoring the pool.

  • Track retirement independently of the error score and persist it in SessionModel.
  • Default the new flag to False when loading older saved state.
  • Make repeated retire() calls leave the error score unchanged.
  • Document the behavior change in the v2 migration guide.

Issues

Closes #2312

Testing

  • Reproduced the original behavior on unchanged v2: a fresh session becomes usable after retire() followed by mark_good() (error score 3.0 -> 2.5). Two regression tests failed before the fix.
  • Added six tests covering terminal retirement, JSON round trips, older saved state, repeated retirement, pool lookups, and pool persistence/reopening.
  • uv run pytest tests/unit/sessions tests/unit/crawlers/_basic -q --timeout=90: 193 passed, 1 existing skip on Windows / Python 3.12.13.
  • Repository-wide Ruff lint and formatting checks passed.
  • Type checks passed for the changed modules and tests. The full repository check passed with --python-platform linux.
  • Both pre-commit hooks passed using a temporary, untracked ty configuration matching the repository settings with the Linux target. That configuration was removed and is not part of this PR.

The default Windows repository-wide type check reports three errors in the unchanged tests/unit/_utils/test_system.py involving ForkContext, ForkServerContext, and BaseContext.Process. Full unit and platform end-to-end suites were not run.

Checklist

  • CI passed

Persist retirement independently of error scores and cover session and pool restoration.

BREAKING CHANGE: Retired sessions cannot become usable again after mark_good().
@github-actions github-actions Bot added the t-tooling Issues with this label are in the ownership of the tooling team. label Oct 21, 2026

@vdusek vdusek left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two minor things

Comment thread docs/upgrading/upgrading_to_v2.md Outdated

This page summarizes the breaking changes between Crawlee for Python v1 and v2.

## Session retirement is permanent

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: The other v2 sections were added at the end of this page. Could you move this one after the last section to match?

Comment thread tests/unit/sessions/test_session.py Outdated
def test_session_without_retirement_field_can_be_restored() -> None:
"""Saved sessions without a retirement field remain loadable."""
state = Session().get_state(as_dict=True)
state.pop('retired', None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: get_state() always includes retired, so the default isn't needed. With del, the test fails if the field is renamed, so it can't silently stop covering old state.

Suggested change
state.pop('retired', None)
del state['retired']

@vdusek
vdusek requested a review from Pijukatel October 26, 2026 14:49

@vdusek vdusek left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A three more comments, regarding comparision with Crawlee JS

Comment thread src/crawlee/sessions/_session.py Outdated
"""Indicate whether the session is blocked based on the error score.."""
return self._error_score >= self._max_error_score
"""Indicate whether the session is retired or blocked by its error score."""
return self._retired or self._error_score >= self._max_error_score

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggestion: JS v4 checks the flag in isUsable() and keeps isBlocked() score-only, so isBlocked() still separates a burned session from one that just expired or hit its usage limit. The self-retire in markGood()/markBad() only sets the flag there. Here mark_good()/mark_bad() self-retire through retire(), so an expired session also gets max_error_score added and reports is_blocked. Could we check _retired in is_usable and have the self-retire path set only the flag?

Comment thread docs/upgrading/upgrading_to_v2.md Outdated

## Session retirement is permanent

`Session.retire()` permanently blocks the session. Calling `mark_good()` can still lower its error score, but cannot make it usable again. Create a new session to replace a retired one. Repeated calls to `retire()` leave the error score unchanged.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggestion: In JS, markGood() returns early on a retired session, so neither the usage count nor the error score changes. An if self._retired: return at the top of mark_good() would match that, and this sentence could go.

self._usage_count = usage_count
self._max_usage_count = max_usage_count
self._error_score = error_score
self._retired = retired

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: JS exposes a public retired getter. A retired property here would let callers read the state without going through get_state().

@gauravch-code

gauravch-code commented Oct 26, 2026 •

Copy link
Copy Markdown
Author

Thanks for the careful review, @vdusek! I addressed the five comments in 261dab6:

  • Retirement is checked in is_usable; is_blocked depends only on the error score.
  • Automatic retirement sets only the retired flag, without adding a blocking penalty.
  • mark_good returns immediately for retired sessions, preserving both score and usage count.
  • Added the public retired property and regression coverage for expiration, usage limits, error limits, and restored retired state.
  • Moved the migration section to the end and changed the legacy-state test to delete the retired field explicitly.

All 49 session tests, full repository lint, and the migration Markdown lint pass. The full type check passes targeting Linux; native Windows reports three unrelated multiprocessing fork-type errors in tests/unit/_utils/test_system.py. Both commit hooks passed using an external Linux-targeted type configuration with the same repository rules.

Happy to adjust anything further. Thanks again!

CI note: the new factory-review approval lookup failed because GitHub returned HTTP 502, while the AI review job passed. GitHub requires repository admin rights to rerun the failed job, so I could not retry it from my fork. Could you rerun that lookup when you have a chance?

Comment on lines +233 to +235
if not self._retired:
self._retired = True
self._error_score += self._max_error_score

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is there any point in doing this for retired session?

self._error_score += self._max_error_score

if not, I would suggest to do just

def retire(self)
    """..."""
    self._retired = True

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t-tooling Issues with this label are in the ownership of the tooling team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants