fluidlayer 2.1.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/PKG-INFO +1 -1
- fluidlayer-2.2.0/deployment/__init__.py +1 -0
- fluidlayer-2.2.0/deployment/session_registry.py +178 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/CHANGELOG.md +11 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/__init__.py +1 -1
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer.egg-info/PKG-INFO +1 -1
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer.egg-info/SOURCES.txt +3 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer.egg-info/top_level.txt +1 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/pyproject.toml +2 -2
- fluidlayer-2.2.0/tests/test_session_registry.py +126 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/CITATION.cff +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/LICENSE +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/MANIFEST.in +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/Makefile +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/README.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/API.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/BLOG.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/COMPARISON.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/DESIGN_pinned_memory.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/FRAMEWORK.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/GETTING_STARTED.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/GRAFT.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/LIMITATION.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/MILESTONES.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/NEGATIVE-RESULTS.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/POSITIONING.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/PROCESS.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/README.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/REPRODUCING.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/TEN-MILLION.md +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/index.html +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/examples/appkit/attach_layer.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/examples/appkit/load_model.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/examples/gate_demo.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/examples/graft_demo.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/examples/quickstart.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e10_gate.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e11_exact.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e12_router.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e13_knn_baseline.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e14_real_gpt2.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e15_scale_model.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e1_prior_perplexity.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e2_scaling.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e4_gbalance.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e5_graft.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e6_graft.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e6_real_corpus.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e7_lossy_fix.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e9_ten_million.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/gpu_graft.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/pretrain_prior.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/run_all.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/autodiff.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/blocks.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/bpe.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/data.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/diffusion.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/gpt2_model.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/gpt2_tokenizer.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/graft.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/losses.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/memory.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/model.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/optim.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/py.typed +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/qwen2_model.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/sheaf.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/topology.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/torch_graft.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer.egg-info/dependency_links.txt +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer.egg-info/requires.txt +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer_single.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/requirements.txt +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/run_without_pytest.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/selftest.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/setup.cfg +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/conftest.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/test_bpe.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/test_graft.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/test_memory.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/test_qwen2_model.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/test_selftest.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/test_topology.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/test_torch_parity.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tools/_bundle_template.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tools/bundle_single_file.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tools/download_gpt2.py +0 -0
- {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tools/fetch_pypi.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Optional deployment helpers for applications embedding FluidLayer."""
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Bounded, process-local isolation for stateful per-session memory branches.
|
|
2
|
+
|
|
3
|
+
This helper is an application integration aid, not an authentication system
|
|
4
|
+
or a cross-process session store. Applications must derive identifiers from
|
|
5
|
+
their authenticated identity layer and must not expose branch objects beyond
|
|
6
|
+
the lease that owns them.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from collections.abc import Iterator
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Callable, Generic, Protocol, TypeVar
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Resettable(Protocol):
|
|
21
|
+
"""The minimum interface required from a stateful branch."""
|
|
22
|
+
|
|
23
|
+
def reset(self) -> None:
|
|
24
|
+
"""Clear all state associated with the branch."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
BranchT = TypeVar("BranchT", bound=Resettable)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SessionRegistryError(RuntimeError):
|
|
31
|
+
"""Base exception for session-registry lifecycle errors."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SessionCapacityError(SessionRegistryError):
|
|
35
|
+
"""Raised when the registry is full of unexpired sessions."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SessionBusyError(SessionRegistryError):
|
|
39
|
+
"""Raised when a caller tries to reset or close an active session."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class _Session(Generic[BranchT]):
|
|
44
|
+
branch: BranchT
|
|
45
|
+
last_used: float
|
|
46
|
+
active_leases: int = 0
|
|
47
|
+
lock: threading.RLock = field(default_factory=threading.RLock)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SessionRegistry(Generic[BranchT]):
|
|
51
|
+
"""Manage separate stateful branches for `(tenant_id, session_id)` keys.
|
|
52
|
+
|
|
53
|
+
Each lease holds a per-session lock for its whole duration. This prevents
|
|
54
|
+
concurrent requests from interleaving writes to one branch while allowing
|
|
55
|
+
independent sessions to proceed concurrently. Expired idle sessions are
|
|
56
|
+
removed when the registry is next accessed. Capacity is a hard bound:
|
|
57
|
+
active or unexpired sessions are never silently evicted.
|
|
58
|
+
|
|
59
|
+
The registry is process-local and volatile. For multiple workers, use
|
|
60
|
+
worker affinity or an application-owned session service; do not serialize
|
|
61
|
+
live branch objects and assume they are safe to restore.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
branch_factory: Callable[[], BranchT],
|
|
67
|
+
*,
|
|
68
|
+
max_sessions: int = 1024,
|
|
69
|
+
ttl_seconds: float = 1800.0,
|
|
70
|
+
clock: Callable[[], float] = time.monotonic,
|
|
71
|
+
) -> None:
|
|
72
|
+
if not callable(branch_factory):
|
|
73
|
+
raise TypeError("branch_factory must be callable")
|
|
74
|
+
if isinstance(max_sessions, bool) or not isinstance(max_sessions, int):
|
|
75
|
+
raise TypeError("max_sessions must be an integer")
|
|
76
|
+
if max_sessions <= 0:
|
|
77
|
+
raise ValueError("max_sessions must be greater than zero")
|
|
78
|
+
if isinstance(ttl_seconds, bool) or not isinstance(ttl_seconds, (int, float)):
|
|
79
|
+
raise TypeError("ttl_seconds must be a finite positive number")
|
|
80
|
+
if not math.isfinite(ttl_seconds) or ttl_seconds <= 0:
|
|
81
|
+
raise ValueError("ttl_seconds must be a finite positive number")
|
|
82
|
+
if not callable(clock):
|
|
83
|
+
raise TypeError("clock must be callable")
|
|
84
|
+
|
|
85
|
+
self._branch_factory = branch_factory
|
|
86
|
+
self._max_sessions = max_sessions
|
|
87
|
+
self._ttl_seconds = float(ttl_seconds)
|
|
88
|
+
self._clock = clock
|
|
89
|
+
self._sessions: dict[tuple[str, str], _Session[BranchT]] = {}
|
|
90
|
+
self._lock = threading.RLock()
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _validate_id(value: str, name: str) -> str:
|
|
94
|
+
if not isinstance(value, str):
|
|
95
|
+
raise TypeError(f"{name} must be a string")
|
|
96
|
+
if not value.strip():
|
|
97
|
+
raise ValueError(f"{name} must not be empty")
|
|
98
|
+
return value
|
|
99
|
+
|
|
100
|
+
def _expire_idle_sessions(self, now: float) -> None:
|
|
101
|
+
expired = [
|
|
102
|
+
key
|
|
103
|
+
for key, session in self._sessions.items()
|
|
104
|
+
if session.active_leases == 0
|
|
105
|
+
and now - session.last_used >= self._ttl_seconds
|
|
106
|
+
]
|
|
107
|
+
for key in expired:
|
|
108
|
+
del self._sessions[key]
|
|
109
|
+
|
|
110
|
+
@contextmanager
|
|
111
|
+
def lease(self, tenant_id: str, session_id: str) -> Iterator[BranchT]:
|
|
112
|
+
"""Yield the branch for one identity pair, serialized per session."""
|
|
113
|
+
tenant = self._validate_id(tenant_id, "tenant_id")
|
|
114
|
+
session_name = self._validate_id(session_id, "session_id")
|
|
115
|
+
key = (tenant, session_name)
|
|
116
|
+
now = self._clock()
|
|
117
|
+
|
|
118
|
+
with self._lock:
|
|
119
|
+
self._expire_idle_sessions(now)
|
|
120
|
+
session = self._sessions.get(key)
|
|
121
|
+
if session is None:
|
|
122
|
+
if len(self._sessions) >= self._max_sessions:
|
|
123
|
+
raise SessionCapacityError(
|
|
124
|
+
f"session registry is at capacity "
|
|
125
|
+
f"({self._max_sessions}); retry after sessions expire "
|
|
126
|
+
"or close an idle session"
|
|
127
|
+
)
|
|
128
|
+
session = _Session(self._branch_factory(), last_used=now)
|
|
129
|
+
self._sessions[key] = session
|
|
130
|
+
session.active_leases += 1
|
|
131
|
+
session.last_used = now
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
with session.lock:
|
|
135
|
+
yield session.branch
|
|
136
|
+
finally:
|
|
137
|
+
with self._lock:
|
|
138
|
+
session.active_leases -= 1
|
|
139
|
+
session.last_used = self._clock()
|
|
140
|
+
|
|
141
|
+
def reset_session(self, tenant_id: str, session_id: str) -> bool:
|
|
142
|
+
"""Reset an idle session; return False if it does not exist."""
|
|
143
|
+
key = (
|
|
144
|
+
self._validate_id(tenant_id, "tenant_id"),
|
|
145
|
+
self._validate_id(session_id, "session_id"),
|
|
146
|
+
)
|
|
147
|
+
with self._lock:
|
|
148
|
+
self._expire_idle_sessions(self._clock())
|
|
149
|
+
session = self._sessions.get(key)
|
|
150
|
+
if session is None:
|
|
151
|
+
return False
|
|
152
|
+
if session.active_leases:
|
|
153
|
+
raise SessionBusyError("cannot reset a session with an active lease")
|
|
154
|
+
session.branch.reset()
|
|
155
|
+
session.last_used = self._clock()
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
def close_session(self, tenant_id: str, session_id: str) -> bool:
|
|
159
|
+
"""Remove an idle session; return False if it does not exist."""
|
|
160
|
+
key = (
|
|
161
|
+
self._validate_id(tenant_id, "tenant_id"),
|
|
162
|
+
self._validate_id(session_id, "session_id"),
|
|
163
|
+
)
|
|
164
|
+
with self._lock:
|
|
165
|
+
session = self._sessions.get(key)
|
|
166
|
+
if session is None:
|
|
167
|
+
return False
|
|
168
|
+
if session.active_leases:
|
|
169
|
+
raise SessionBusyError("cannot close a session with an active lease")
|
|
170
|
+
del self._sessions[key]
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def session_count(self) -> int:
|
|
175
|
+
"""Return the number of registered sessions after expiring idle entries."""
|
|
176
|
+
with self._lock:
|
|
177
|
+
self._expire_idle_sessions(self._clock())
|
|
178
|
+
return len(self._sessions)
|
|
@@ -6,6 +6,17 @@ uses [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.2.0] - 2026-11-03
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Include the optional deployment session-isolation companion in the
|
|
14
|
+
published wheel and source distribution.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Add clean-install CI verification for the deployment package.
|
|
19
|
+
|
|
9
20
|
## [2.1.0] - 2026-11-03
|
|
10
21
|
|
|
11
22
|
### Added
|
|
@@ -8,6 +8,8 @@ pyproject.toml
|
|
|
8
8
|
requirements.txt
|
|
9
9
|
run_without_pytest.py
|
|
10
10
|
selftest.py
|
|
11
|
+
deployment/__init__.py
|
|
12
|
+
deployment/session_registry.py
|
|
11
13
|
docs/API.md
|
|
12
14
|
docs/BLOG.md
|
|
13
15
|
docs/CHANGELOG.md
|
|
@@ -76,6 +78,7 @@ tests/test_graft.py
|
|
|
76
78
|
tests/test_memory.py
|
|
77
79
|
tests/test_qwen2_model.py
|
|
78
80
|
tests/test_selftest.py
|
|
81
|
+
tests/test_session_registry.py
|
|
79
82
|
tests/test_topology.py
|
|
80
83
|
tests/test_torch_parity.py
|
|
81
84
|
tools/_bundle_template.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fluidlayer"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2.0"
|
|
8
8
|
description = "A research memory layer for language models with optional GPT-2 and Qwen2 adapters"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -50,7 +50,7 @@ Issues = "https://github.com/kapildhinwa2010-dev/fluidtech/issues"
|
|
|
50
50
|
Changelog = "https://github.com/kapildhinwa2010-dev/fluidtech/blob/main/docs/CHANGELOG.md"
|
|
51
51
|
|
|
52
52
|
[tool.setuptools]
|
|
53
|
-
packages = ["fluidlayer"]
|
|
53
|
+
packages = ["fluidlayer", "deployment"]
|
|
54
54
|
include-package-data = true
|
|
55
55
|
|
|
56
56
|
[tool.setuptools.package-data]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from deployment.session_registry import (
|
|
6
|
+
SessionBusyError,
|
|
7
|
+
SessionCapacityError,
|
|
8
|
+
SessionRegistry,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DummyBranch:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.values = []
|
|
15
|
+
self.reset_count = 0
|
|
16
|
+
|
|
17
|
+
def reset(self):
|
|
18
|
+
self.values.clear()
|
|
19
|
+
self.reset_count += 1
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_sessions_are_isolated_by_tenant_and_session():
|
|
23
|
+
registry = SessionRegistry(DummyBranch)
|
|
24
|
+
|
|
25
|
+
with registry.lease("tenant-a", "conversation") as first:
|
|
26
|
+
first.values.append("private")
|
|
27
|
+
with registry.lease("tenant-b", "conversation") as second:
|
|
28
|
+
assert second.values == []
|
|
29
|
+
second.values.append("other")
|
|
30
|
+
with registry.lease("tenant-a", "conversation") as first_again:
|
|
31
|
+
assert first_again is first
|
|
32
|
+
assert first_again.values == ["private"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_capacity_is_a_hard_bound_and_expired_sessions_are_reclaimed():
|
|
36
|
+
now = [0.0]
|
|
37
|
+
registry = SessionRegistry(
|
|
38
|
+
DummyBranch, max_sessions=1, ttl_seconds=5, clock=lambda: now[0]
|
|
39
|
+
)
|
|
40
|
+
with registry.lease("tenant", "one"):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
with pytest.raises(SessionCapacityError):
|
|
44
|
+
with registry.lease("tenant", "two"):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
now[0] = 5.0
|
|
48
|
+
with registry.lease("tenant", "two"):
|
|
49
|
+
assert registry.session_count == 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_active_leases_are_not_expired():
|
|
53
|
+
now = [0.0]
|
|
54
|
+
registry = SessionRegistry(DummyBranch, ttl_seconds=2, clock=lambda: now[0])
|
|
55
|
+
|
|
56
|
+
with registry.lease("tenant", "one"):
|
|
57
|
+
now[0] = 100.0
|
|
58
|
+
assert registry.session_count == 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_reset_and_close_require_an_idle_session():
|
|
62
|
+
registry = SessionRegistry(DummyBranch)
|
|
63
|
+
with registry.lease("tenant", "one") as branch:
|
|
64
|
+
branch.values.append("value")
|
|
65
|
+
with pytest.raises(SessionBusyError):
|
|
66
|
+
registry.reset_session("tenant", "one")
|
|
67
|
+
with pytest.raises(SessionBusyError):
|
|
68
|
+
registry.close_session("tenant", "one")
|
|
69
|
+
|
|
70
|
+
assert registry.reset_session("tenant", "one")
|
|
71
|
+
with registry.lease("tenant", "one") as reset_branch:
|
|
72
|
+
assert reset_branch.values == []
|
|
73
|
+
assert reset_branch.reset_count == 1
|
|
74
|
+
assert registry.close_session("tenant", "one")
|
|
75
|
+
assert not registry.close_session("tenant", "one")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_requests_for_one_session_are_serialized():
|
|
79
|
+
registry = SessionRegistry(DummyBranch)
|
|
80
|
+
first_entered = threading.Event()
|
|
81
|
+
release_first = threading.Event()
|
|
82
|
+
second_entered = threading.Event()
|
|
83
|
+
|
|
84
|
+
def hold_first_lease():
|
|
85
|
+
with registry.lease("tenant", "same"):
|
|
86
|
+
first_entered.set()
|
|
87
|
+
assert release_first.wait(timeout=2)
|
|
88
|
+
|
|
89
|
+
def acquire_second_lease():
|
|
90
|
+
with registry.lease("tenant", "same"):
|
|
91
|
+
second_entered.set()
|
|
92
|
+
|
|
93
|
+
first = threading.Thread(target=hold_first_lease)
|
|
94
|
+
second = threading.Thread(target=acquire_second_lease)
|
|
95
|
+
first.start()
|
|
96
|
+
assert first_entered.wait(timeout=2)
|
|
97
|
+
second.start()
|
|
98
|
+
assert not second_entered.wait(timeout=0.05)
|
|
99
|
+
release_first.set()
|
|
100
|
+
first.join(timeout=2)
|
|
101
|
+
second.join(timeout=2)
|
|
102
|
+
|
|
103
|
+
assert not first.is_alive()
|
|
104
|
+
assert not second.is_alive()
|
|
105
|
+
assert second_entered.is_set()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pytest.mark.parametrize(
|
|
109
|
+
("kwargs", "error"),
|
|
110
|
+
[
|
|
111
|
+
({"max_sessions": 0}, ValueError),
|
|
112
|
+
({"max_sessions": True}, TypeError),
|
|
113
|
+
({"ttl_seconds": 0}, ValueError),
|
|
114
|
+
({"ttl_seconds": float("inf")}, ValueError),
|
|
115
|
+
],
|
|
116
|
+
)
|
|
117
|
+
def test_configuration_is_validated(kwargs, error):
|
|
118
|
+
with pytest.raises(error):
|
|
119
|
+
SessionRegistry(DummyBranch, **kwargs)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_blank_session_identifiers_are_rejected():
|
|
123
|
+
registry = SessionRegistry(DummyBranch)
|
|
124
|
+
with pytest.raises(ValueError):
|
|
125
|
+
with registry.lease("tenant", " "):
|
|
126
|
+
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|