Compare commits

...

2 Commits

Author SHA1 Message Date
63ef3a8313 chore(release): v0.7.30 — bus-error fix (revert hydrate-thread-per-view + idle eager_hydrate skip)
All checks were successful
boundary-lint / PR boundary-claim (Lint (push) Has been skipped
boundary-lint / ban-list lint (Lint (push) Successful in 17s
boundary-lint / duplication-deadline (Layer 1/2) (push) Successful in 18s
ci / mutation test (broker) (push) Has been skipped
ci / test-health gate (push) Successful in 17s
Release Publish (Gitea session_helper) / verify-release-tag (push) Successful in 17s
ci / rust debug (push) Successful in 2m50s
ci / rust release (push) Successful in 2m52s
Release Publish (Gitea session_helper) / publish-linux-x86_64 (push) Successful in 3m36s
ci / python (push) Successful in 1m23s
Hot-fix on top of v0.7.29:

* Revert v0.7.29 ``hydrate_open_file`` thread-per-view — rapid tab-
  switching spawned 7+ concurrent threads that contended on Sublime's
  non-thread-safe View API and bus-errored macOS Sublime. Hydrate
  rides the shared background worker again; the long-running tasks
  that originally caused the head-of-line block (eager_hydrate,
  refresh_git_state) stay on their own threads, so sequential hydrate
  through the shared queue is safe again.
* ``_schedule_eager_hydrate_if_needed`` runs the cheap local
  ``find_candidates`` scan first and bails out before spawning a
  thread when the workspace has zero placeholders (eliminates the
  ``hydrated:0 skipped:0 failed:0`` trace spam fired on every
  workspace activation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:34:38 +09:00
05c08e3223 fix(commands): revert hydrate to shared queue + skip eager_hydrate when no placeholders
Crash report (v0.7.29 debug-trace.log)
--------------------------------------

Rapid tab-switching spawned 7-8 concurrent ``hydrate_open_file`` daemon
threads (one per view). Each called the non-thread-safe Sublime View
API under a per-view PendingHandle on the shared broker session. After
~7 concurrent in-flight ``file/read`` calls landed in the same 2 ms
window the Sublime process bus-errored on macOS::

    19474 bus error  /Applications/Sublime Text.app/Contents/MacOS/sublime_text

Fix 1 — revert v0.7.29 hydrate-on-own-thread
--------------------------------------------

``_schedule_sidebar_placeholder_hydrate`` goes back to
``_run_in_background(prioritize=True, task_key=hydrate:<path>)``. The
shared background worker is already free of long-running tasks
(eager_hydrate stayed on its own thread from v0.7.28; refresh_git_state
on its own from v0.7.29) so the head-of-line blocker that motivated
v0.7.29's split is gone — sequential dispatch through the queue
keeps concurrent broker.request callers bounded without re-introducing
a head-of-line block.

Fix 2 — eager_hydrate fast-path skip when no placeholders
---------------------------------------------------------

Workspace activation fires on every tab switch and v0.7.28's per-key
in-flight set only blocked parallel passes — the empty pass for an
idle workspace (no zero-byte Cargo.toml etc.) finished in ~10 ms then
the next tab switch spawned another empty pass, polluting the trace
with ``hydrated:0 skipped:0 failed:0``. ``_schedule_eager_hydrate_if_
needed`` now runs ``_rust_ffi.eager_hydrate_find_candidates`` (cheap
local fs scan, milliseconds) before deciding whether to spawn the
worker thread. Empty result → no thread, no trace spam.

Tests
-----

Two hydrate-schedule tests reverted to the v0.7.28 shape.
1298 Python tests pass; boundary lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:34:00 +09:00
6 changed files with 50 additions and 71 deletions

View File

@@ -1,6 +1,6 @@
[project]
name = "sessions-sublime"
version = "0.7.29"
version = "0.7.30"
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.29"
version = "0.7.30"
dependencies = [
"base64",
"glob",
@@ -432,7 +432,7 @@ dependencies = [
[[package]]
name = "session_helper"
version = "0.7.29"
version = "0.7.30"
dependencies = [
"base64",
"notify",
@@ -443,7 +443,7 @@ dependencies = [
[[package]]
name = "session_protocol"
version = "0.7.29"
version = "0.7.30"
dependencies = [
"base64",
"serde",
@@ -452,14 +452,14 @@ dependencies = [
[[package]]
name = "sessions_askpass"
version = "0.7.29"
version = "0.7.30"
dependencies = [
"tempfile",
]
[[package]]
name = "sessions_native"
version = "0.7.29"
version = "0.7.30"
dependencies = [
"base64",
"serde_json",
@@ -772,7 +772,7 @@ dependencies = [
[[package]]
name = "workspace_identity"
version = "0.7.29"
version = "0.7.30"
[[package]]
name = "zmij"

View File

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

View File

@@ -1860,20 +1860,20 @@ def _schedule_sidebar_placeholder_hydrate(view: object) -> None:
_set_timeout(finish, 50)
# 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()
# Run on the shared background worker — sequential dispatch keeps the
# number of concurrent broker.request callers bounded (one). v0.7.29
# tried thread-per-view but rapid tab-switching spawned many threads
# that contended on Sublime's non-thread-safe View API and bus-
# errored on macOS. Long-running tasks that previously HOL-blocked
# this lane (``eager_hydrate``, ``sessions.refresh_git_state``) now
# have their own dedicated threads, so single-worker queueing is
# safe again.
_run_in_background(
work,
prioritize=True,
task_key="hydrate:{}".format(path_str),
task_label="hydrate_open_file",
)
class SessionsSidebarPlaceholderHydrateListener(sublime_plugin.EventListener):
@@ -3592,6 +3592,19 @@ def _schedule_eager_hydrate_if_needed(
if not basenames:
return
cache_key = context.cache_key
# Fast-path skip: every workspace activation re-fires this scheduler
# and an idle workspace with zero placeholders would otherwise spawn a
# do-nothing thread on every tab switch (see the spam of
# ``hydrated:0 skipped_existing:0 failed:0`` traces). Run a cheap
# local-fs scan to bail out before threading.
try:
candidates = _rust_ffi.eager_hydrate_find_candidates(
str(context.local_cache_root), list(basenames)
)
except Exception:
candidates = ()
if not candidates:
return
if bool(getattr(sublime, "_sessions_test_sync", False)):
_eager_hydrate_workspace(window, context, basenames)
return

View File

@@ -644,16 +644,13 @@ def test_hydrate_precheck_error_skips_read_for_active_view(
assert opened_calls == []
def test_hydrate_schedule_runs_synchronously_in_test_mode(
def test_hydrate_schedule_sets_path_scoped_task_key(
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."""
"""v0.7.30 reverts hydrate-on-open back to the shared background
queue (single worker, sequential dispatch) — v0.7.29's per-view
thread spawning crashed on rapid tab-switching due to concurrent
Sublime View API calls. The queue's ``task_key`` dedup is back."""
context = commands._WorkspaceContext(
settings=SessionsSettings(),
recent_entry=RecentWorkspace(
@@ -679,38 +676,22 @@ def test_hydrate_schedule_runs_synchronously_in_test_mode(
)
view.id = lambda: 43
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"],
)
captured: List[Optional[str]] = []
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,
"_precheck_remote_file_openability",
lambda **kwargs: commands._HydratePrecheckOutcome(
proceed=True, stat_metadata=None
),
"_run_in_background",
lambda target, *args, **kwargs: captured.append(kwargs.get("task_key")),
)
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_remotes == ["/srv/ws/app.py"]
assert 43 in commands._HYDRATE_IN_FLIGHT
assert captured == ["hydrate:{}".format(str(local_file))]
def test_hydrate_schedule_fetches_for_nonexistent_cache_file(
@@ -752,38 +733,23 @@ def test_hydrate_schedule_fetches_for_nonexistent_cache_file(
)
view.id = lambda: 99
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"],
)
captured: List[Optional[str]] = []
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,
"_precheck_remote_file_openability",
lambda **kwargs: commands._HydratePrecheckOutcome(
proceed=True, stat_metadata=None
),
"_run_in_background",
lambda target, *args, **kwargs: captured.append(kwargs.get("task_key")),
)
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_remotes == ["/srv/ws/src/module.py"], (
"goto-def on uncached file must still trigger a hydrate fetch"
assert captured == ["hydrate:{}".format(str(local_file))], (
"goto-def on uncached file must still enqueue a hydrate fetch"
)

2
uv.lock generated
View File

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