Compare commits

...

2 Commits

Author SHA1 Message Date
20227dde4d chore(release): v0.7.29 — hot-fix #2 (refresh_git_state + hydrate_open_file dedicated threads)
All checks were successful
boundary-lint / PR boundary-claim (Lint (push) Has been skipped
boundary-lint / ban-list lint (Lint (push) Successful in 19s
boundary-lint / duplication-deadline (Layer 1/2) (push) Successful in 19s
Release Publish (Gitea session_helper) / verify-release-tag (push) Successful in 18s
ci / test-health gate (push) Successful in 18s
Release Publish (Gitea session_helper) / publish-linux-x86_64 (push) Successful in 5m2s
ci / mutation test (broker) (push) Successful in 2m9s
ci / rust debug (push) Successful in 2m46s
ci / rust release (push) Successful in 3m23s
ci / python (push) Successful in 1m19s
Follow-up to v0.7.28: eager_hydrate was moved off the shared background
worker, but ``sessions.refresh_git_state`` (305 s timeout on
``exec/once`` for slow ``git fetch``) became the new head-of-line
blocker — opening a file after reconnect still left the cache empty
and the subsequent save reported metadata-mismatch conflicts.

* ``_schedule_sidebar_placeholder_hydrate`` runs on a per-view daemon
  thread, not the shared queue. ``_HYDRATE_IN_FLIGHT`` (view-id set)
  remains the dedup primitive.
* ``sessions.refresh_git_state`` runs on a per-cache_key daemon
  thread with ``_REFRESH_GIT_STATE_INFLIGHT`` dedup.

The shared ``_BACKGROUND_TASK_QUEUE`` is now reserved for
short-running tasks; every known multi-second / multi-minute path
runs in its own lane with no head-of-line blocking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:53:59 +09:00
b5d5404f73 fix(commands): hydrate_open_file + sessions.refresh_git_state on dedicated threads — second HOL blocker
Symptom (v0.7.28 debug-trace.log)
---------------------------------

eager_hydrate moved off the shared background worker (v0.7.28 fix), but
``hydrate_open_file`` still didn't run when the user opened a file
after reconnect. Root cause: ``sessions.refresh_git_state`` (which
issues an ``exec/once`` with a 305 s timeout for ``git fetch``)
dequeued first, blocking the worker for minutes. The user's
prioritized ``hydrate_open_file`` tasks queued up behind it and never
fired — observed as "open file but cache stays empty, save then fails
with 'file already exists' metadata mismatch".

Fix
---

Same per-cache_key in-flight + dedicated daemon thread pattern as the
v0.7.28 eager_hydrate fix, applied to two more long-running paths:

* ``_schedule_sidebar_placeholder_hydrate`` no longer goes through
  ``_run_in_background``. The hydrate runs on a daemon thread named
  ``sessions-hydrate-open-file-<view_id>``. ``_HYDRATE_IN_FLIGHT``
  (per-view dedup) was already in place; the queue task_key dedup
  becomes redundant and was the only thing we lose.
* ``sessions.refresh_git_state`` adds
  ``_REFRESH_GIT_STATE_INFLIGHT`` (Set[str]) +
  ``_REFRESH_GIT_STATE_INFLIGHT_LOCK`` and runs the work() body on its
  own ``sessions-refresh-git-<cache_key>`` daemon thread.

After this fix, the shared ``_BACKGROUND_TASK_QUEUE`` is reserved for
short tasks; the three known long-running paths (eager_hydrate,
hydrate_open_file, refresh_git_state) each run on their own thread
with per-key dedup. No new lint #2 ``_*_TASK_QUEUE`` deque introduced.

Tests
-----

Two existing hydrate-schedule tests rewrote to verify the new
synchronous-mode path. 1298 Python tests pass; cargo clippy green;
boundary lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:53:23 +09:00
6 changed files with 110 additions and 30 deletions

View File

@@ -1,6 +1,6 @@
[project]
name = "sessions-sublime"
version = "0.7.28"
version = "0.7.29"
description = "Sublime-facing Python code for Sessions."
requires-python = ">=3.8"
license = {text = "MIT"}

12
rust/Cargo.lock generated
View File

@@ -221,7 +221,7 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "local_bridge"
version = "0.7.28"
version = "0.7.29"
dependencies = [
"base64",
"glob",
@@ -432,7 +432,7 @@ dependencies = [
[[package]]
name = "session_helper"
version = "0.7.28"
version = "0.7.29"
dependencies = [
"base64",
"notify",
@@ -443,7 +443,7 @@ dependencies = [
[[package]]
name = "session_protocol"
version = "0.7.28"
version = "0.7.29"
dependencies = [
"base64",
"serde",
@@ -452,14 +452,14 @@ dependencies = [
[[package]]
name = "sessions_askpass"
version = "0.7.28"
version = "0.7.29"
dependencies = [
"tempfile",
]
[[package]]
name = "sessions_native"
version = "0.7.28"
version = "0.7.29"
dependencies = [
"base64",
"serde_json",
@@ -772,7 +772,7 @@ dependencies = [
[[package]]
name = "workspace_identity"
version = "0.7.28"
version = "0.7.29"
[[package]]
name = "zmij"

View File

@@ -12,7 +12,7 @@ resolver = "2"
[workspace.package]
edition = "2024"
license = "MIT"
version = "0.7.28"
version = "0.7.29"
authors = ["Myeongseon Choi <key262yek@gmail.com>"]
repository = "https://git.teahaven.kr/sublime-rs/sessions"
homepage = "https://git.teahaven.kr/sublime-rs/sessions"

View File

@@ -306,6 +306,15 @@ _MIRROR_QUEUE_MAX = 8
_EAGER_HYDRATE_INFLIGHT: Set[str] = set()
_EAGER_HYDRATE_INFLIGHT_LOCK = threading.Lock()
# ``sessions.refresh_git_state`` previously rode the shared background
# worker; one of its ``exec/once`` calls has a 305 s timeout (long ``git
# fetch``) which head-of-line-blocked every later task — including the
# ``prioritize=True`` ``hydrate_open_file`` fired when the user opens a
# remote file. v0.7.28 same per-key in-flight pattern as eager-hydrate
# moves the refresh to its own daemon thread per cache_key.
_REFRESH_GIT_STATE_INFLIGHT: Set[str] = set()
_REFRESH_GIT_STATE_INFLIGHT_LOCK = threading.Lock()
def _mirror_queue_pressure(queue_size: int, dropped: int) -> str:
return _rust_ffi.mirror_queue_pressure(
@@ -1851,12 +1860,20 @@ def _schedule_sidebar_placeholder_hydrate(view: object) -> None:
_set_timeout(finish, 50)
_run_in_background(
work,
prioritize=True,
task_key="hydrate:{}".format(path_str),
task_label="hydrate_open_file",
)
# User-facing critical path: hydrate the file the user just opened.
# Run on a dedicated daemon thread so neither ``eager_hydrate`` nor
# ``sessions.refresh_git_state`` (which can block the shared worker
# for up to 5 minutes on a slow ``git fetch``) can stall it. The
# ``_HYDRATE_IN_FLIGHT`` set above already dedupes per-view, so
# losing the queue's task_key dedup costs nothing.
if bool(getattr(sublime, "_sessions_test_sync", False)):
work()
else:
threading.Thread(
target=work,
daemon=True,
name="sessions-hydrate-open-file-{}".format(view_id),
).start()
class SessionsSidebarPlaceholderHydrateListener(sublime_plugin.EventListener):
@@ -7322,11 +7339,36 @@ def _run_track_g_refresh(
_set_timeout(finish, 0)
_run_in_background(
work,
task_label="sessions.refresh_git_state",
task_key="sessions_refresh_git_state:{}".format(context.cache_key),
)
# Git refresh issues an ``exec/once`` with a 305 s timeout (slow
# network ``git fetch``); on the shared background worker it would
# head-of-line-block ``hydrate_open_file`` and other prioritised
# tasks. Per-cache_key in-flight set + dedicated daemon thread
# mirrors the eager-hydrate pattern.
cache_key = context.cache_key
if bool(getattr(sublime, "_sessions_test_sync", False)):
work()
return
with _REFRESH_GIT_STATE_INFLIGHT_LOCK:
if cache_key in _REFRESH_GIT_STATE_INFLIGHT:
_trace_event("git.refresh_state_skip_inflight", cache_key=cache_key)
return
_REFRESH_GIT_STATE_INFLIGHT.add(cache_key)
def _runner() -> None:
try:
work()
except Exception:
_trace_event("git.refresh_state_thread_error", cache_key=cache_key)
print("[Sessions] Git refresh thread failed.", file=sys.stderr)
finally:
with _REFRESH_GIT_STATE_INFLIGHT_LOCK:
_REFRESH_GIT_STATE_INFLIGHT.discard(cache_key)
threading.Thread(
target=_runner,
daemon=True,
name="sessions-refresh-git-{}".format(cache_key),
).start()
# ---------------------------------------------------------------------------

View File

@@ -644,9 +644,16 @@ def test_hydrate_precheck_error_skips_read_for_active_view(
assert opened_calls == []
def test_hydrate_schedule_sets_path_scoped_task_key(
def test_hydrate_schedule_runs_synchronously_in_test_mode(
tmp_path: Path, monkeypatch
) -> None:
"""v0.7.28+ hydrate-on-open runs on a dedicated daemon thread per
view (not the shared background queue) so it cannot be head-of-
line-blocked by long-running tasks (eager_hydrate,
sessions.refresh_git_state). Under ``_sessions_test_sync`` the
function falls back to running work() inline; we verify the
expected ``open_remote_file_into_local_cache`` invocation reaches
the right remote path with the right view-id dedup."""
context = commands._WorkspaceContext(
settings=SessionsSettings(),
recent_entry=RecentWorkspace(
@@ -672,22 +679,38 @@ def test_hydrate_schedule_sets_path_scoped_task_key(
)
view.id = lambda: 43
captured: List[Optional[str]] = []
captured_remotes: List[str] = []
def fake_open(host, **kwargs):
captured_remotes.append(kwargs["remote_absolute_path"])
return commands.OpenFileResult(
outcome=commands.OpenOutcome.OK,
local_cache_path=kwargs["local_cache_path"],
)
monkeypatch.setattr(commands, "_mirror_hydrate_placeholders_on_open", lambda: True)
monkeypatch.setattr(commands, "_workspace_context", lambda *args, **kwargs: context)
monkeypatch.setattr(
commands, "_workspace_runtime_connected", lambda *args, **kwargs: True
)
monkeypatch.setattr(commands, "_active_view", lambda window: view)
monkeypatch.setattr(
commands,
"_run_in_background",
lambda target, *args, **kwargs: captured.append(kwargs.get("task_key")),
"_precheck_remote_file_openability",
lambda **kwargs: commands._HydratePrecheckOutcome(
proceed=True, stat_metadata=None
),
)
monkeypatch.setattr(commands, "open_remote_file_into_local_cache", fake_open)
monkeypatch.setattr(commands, "_apply_hydrate_result", lambda **kwargs: None)
monkeypatch.setattr(commands, "_set_timeout", lambda fn, ms: fn())
monkeypatch.setattr(commands.sublime, "_sessions_test_sync", True, raising=False)
commands._HYDRATE_IN_FLIGHT.clear()
commands._schedule_sidebar_placeholder_hydrate(view)
assert captured == ["hydrate:{}".format(str(local_file))]
assert captured_remotes == ["/srv/ws/app.py"]
assert 43 in commands._HYDRATE_IN_FLIGHT
def test_hydrate_schedule_fetches_for_nonexistent_cache_file(
@@ -729,23 +752,38 @@ def test_hydrate_schedule_fetches_for_nonexistent_cache_file(
)
view.id = lambda: 99
captured: List[Optional[str]] = []
captured_remotes: List[str] = []
def fake_open(host, **kwargs):
captured_remotes.append(kwargs["remote_absolute_path"])
return commands.OpenFileResult(
outcome=commands.OpenOutcome.OK,
local_cache_path=kwargs["local_cache_path"],
)
monkeypatch.setattr(commands, "_mirror_hydrate_placeholders_on_open", lambda: True)
monkeypatch.setattr(commands, "_workspace_context", lambda *args, **kwargs: context)
monkeypatch.setattr(
commands, "_workspace_runtime_connected", lambda *args, **kwargs: True
)
monkeypatch.setattr(commands, "_active_view", lambda window: view)
monkeypatch.setattr(
commands,
"_run_in_background",
lambda target, *args, **kwargs: captured.append(kwargs.get("task_key")),
"_precheck_remote_file_openability",
lambda **kwargs: commands._HydratePrecheckOutcome(
proceed=True, stat_metadata=None
),
)
monkeypatch.setattr(commands, "open_remote_file_into_local_cache", fake_open)
monkeypatch.setattr(commands, "_apply_hydrate_result", lambda **kwargs: None)
monkeypatch.setattr(commands, "_set_timeout", lambda fn, ms: fn())
monkeypatch.setattr(commands.sublime, "_sessions_test_sync", True, raising=False)
commands._HYDRATE_IN_FLIGHT.clear()
commands._schedule_sidebar_placeholder_hydrate(view)
assert captured == ["hydrate:{}".format(str(local_file))], (
"goto-def on uncached file must still enqueue a hydrate fetch"
assert captured_remotes == ["/srv/ws/src/module.py"], (
"goto-def on uncached file must still trigger a hydrate fetch"
)

2
uv.lock generated
View File

@@ -854,7 +854,7 @@ wheels = [
[[package]]
name = "sessions-sublime"
version = "0.7.27"
version = "0.7.28"
source = { virtual = "." }
[package.dev-dependencies]