Bobby Claude Opus 5.5 commited on
Commit
7df6873
·
1 Parent(s): ea9eede

Keep grants per nonce so queued and overlapping runs keep their grant

Browse files

Settled grants now live for an hour from settlement instead of expiring
180 s after the request, so a run that waited in the GPU queue still
uses the grant the API already counted. Entries are keyed by nonce and
settled by the nonce in the Space-signed receipt, so a second run in the
same session no longer cancels the first. A granted entry that lapses
before use is denied with a Space-signed refund, and every failed run in
a session gets its own refund.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

access_limits.py CHANGED
@@ -17,6 +17,7 @@ from quota_fallback import (
17
  REFUND,
18
  embed_parent_from,
19
  embed_parents,
 
20
  is_eligible_embed,
21
  issue_ticket,
22
  request_header,
@@ -30,7 +31,9 @@ logger = logging.getLogger(__name__)
30
 
31
  FREE_LIMIT_REASON = "LABS_3DAI_FREE_LIMIT"
32
  GRANT_TTL_SECONDS = 180
33
- MAX_GRANT_SESSIONS = 512
 
 
34
  SIGNUP_URL = "https://app.skyebrowse.com/register"
35
  EMBED_LIMIT_MESSAGE = (
36
  "You have used your free 3D AI generations. Upgrade to a Commercial plan to keep generating."
@@ -79,7 +82,9 @@ def visitor_ip(request: Any) -> str:
79
  @dataclass
80
  class GrantEntry:
81
  nonce: str
 
82
  issued_at: float
 
83
  mode: Optional[str] = None
84
  notice: Optional[str] = None
85
 
@@ -93,21 +98,32 @@ class Access:
93
 
94
 
95
  class GrantBook:
96
- def __init__(self, ttl: float = GRANT_TTL_SECONDS, max_sessions: int = MAX_GRANT_SESSIONS):
 
 
 
 
 
 
97
  self._lock = threading.Lock()
98
  self._entries: Dict[str, GrantEntry] = {}
 
99
  self._ttl = ttl
100
- self._max_sessions = max_sessions
 
 
101
 
102
  def request(self, session_hash: str) -> Optional[Dict[str, str]]:
103
  if not session_hash:
104
  return None
105
  with self._lock:
106
  self._purge()
107
- if len(self._entries) >= self._max_sessions and session_hash not in self._entries:
108
  return None
109
- entry = GrantEntry(uuid4().hex, time.time())
110
- self._entries[session_hash] = entry
 
 
111
  ticket = issue_ticket(entry.nonce, GRANT)
112
  return {"ticket": ticket} if ticket else None
113
 
@@ -116,10 +132,13 @@ class GrantBook:
116
  return
117
  if ack.get("ticket") != receipt.get("ticket"):
118
  return
 
119
  with self._lock:
120
- entry = self._entries.get(session_hash)
121
- if entry is None or entry.mode or entry.notice:
 
122
  return
 
123
  grant = verify_grant(ack.get("grant"), entry.nonce)
124
  if grant is not None:
125
  entry.mode = grant["mode"]
@@ -131,18 +150,45 @@ class GrantBook:
131
  def take(self, session_hash: str) -> Access:
132
  with self._lock:
133
  self._purge()
134
- entry = self._entries.pop(session_hash, None)
135
- if entry is None or (entry.mode is None and entry.notice is None):
136
- return Access(mode="denied", notice=embed_notice(EMBED_UNAVAILABLE_MESSAGE))
137
- if entry.mode is None:
138
- return Access(mode="denied", notice=embed_notice(entry.notice))
 
 
139
  return Access(nonce=entry.nonce)
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  def _purge(self) -> None:
142
- cutoff = time.time() - self._ttl
143
- for key, entry in list(self._entries.items()):
144
- if entry.issued_at < cutoff:
145
- del self._entries[key]
 
 
 
 
 
 
 
 
 
146
 
147
 
148
  class PublicAllowance:
@@ -256,12 +302,15 @@ class AccessPolicy:
256
  self.grants = grants
257
  self.public = public
258
  self._notices: Dict[str, str] = {}
259
- self._refunds: Dict[str, str] = {}
260
  self._lock = threading.Lock()
261
 
262
  def authorize(self, request: Any, session_hash: str) -> Access:
263
  if grants_required(request):
264
- return self.grants.take(session_hash)
 
 
 
265
  if not (self.public.enabled and is_browser_visit(request)):
266
  return Access()
267
  key = self.public.key_for(request)
@@ -278,11 +327,14 @@ class AccessPolicy:
278
  self.public.refund(access.visitor_key)
279
  if access.nonce:
280
  with self._lock:
281
- self._refunds[session_hash] = access.nonce
282
 
283
  def refund_receipt(self, session_hash: str) -> Optional[Dict[str, str]]:
284
  with self._lock:
285
- nonce = self._refunds.pop(session_hash, None)
 
 
 
286
  ticket = issue_ticket(nonce, REFUND) if nonce else None
287
  return {"ticket": ticket} if ticket else None
288
 
 
17
  REFUND,
18
  embed_parent_from,
19
  embed_parents,
20
+ grant_request_nonce,
21
  is_eligible_embed,
22
  issue_ticket,
23
  request_header,
 
31
 
32
  FREE_LIMIT_REASON = "LABS_3DAI_FREE_LIMIT"
33
  GRANT_TTL_SECONDS = 180
34
+ SETTLED_GRANT_TTL_SECONDS = 3600
35
+ MAX_GRANT_ENTRIES = 512
36
+ MAX_GRANTS_PER_SESSION = 8
37
  SIGNUP_URL = "https://app.skyebrowse.com/register"
38
  EMBED_LIMIT_MESSAGE = (
39
  "You have used your free 3D AI generations. Upgrade to a Commercial plan to keep generating."
 
82
  @dataclass
83
  class GrantEntry:
84
  nonce: str
85
+ session_hash: str
86
  issued_at: float
87
+ settled_at: Optional[float] = None
88
  mode: Optional[str] = None
89
  notice: Optional[str] = None
90
 
 
98
 
99
 
100
  class GrantBook:
101
+ def __init__(
102
+ self,
103
+ ttl: float = GRANT_TTL_SECONDS,
104
+ max_entries: int = MAX_GRANT_ENTRIES,
105
+ settled_ttl: float = SETTLED_GRANT_TTL_SECONDS,
106
+ max_per_session: int = MAX_GRANTS_PER_SESSION,
107
+ ):
108
  self._lock = threading.Lock()
109
  self._entries: Dict[str, GrantEntry] = {}
110
+ self._lapsed: Dict[str, List[str]] = {}
111
  self._ttl = ttl
112
+ self._settled_ttl = settled_ttl
113
+ self._max_entries = max_entries
114
+ self._max_per_session = max_per_session
115
 
116
  def request(self, session_hash: str) -> Optional[Dict[str, str]]:
117
  if not session_hash:
118
  return None
119
  with self._lock:
120
  self._purge()
121
+ if len(self._entries) >= self._max_entries:
122
  return None
123
+ if len(self._session_entries(session_hash)) >= self._max_per_session:
124
+ return None
125
+ entry = GrantEntry(uuid4().hex, session_hash, time.time())
126
+ self._entries[entry.nonce] = entry
127
  ticket = issue_ticket(entry.nonce, GRANT)
128
  return {"ticket": ticket} if ticket else None
129
 
 
132
  return
133
  if ack.get("ticket") != receipt.get("ticket"):
134
  return
135
+ nonce = grant_request_nonce(receipt.get("ticket"))
136
  with self._lock:
137
+ self._purge()
138
+ entry = self._entries.get(nonce) if nonce else None
139
+ if entry is None or entry.session_hash != session_hash or entry.settled_at is not None:
140
  return
141
+ entry.settled_at = time.time()
142
  grant = verify_grant(ack.get("grant"), entry.nonce)
143
  if grant is not None:
144
  entry.mode = grant["mode"]
 
150
  def take(self, session_hash: str) -> Access:
151
  with self._lock:
152
  self._purge()
153
+ entry = self._pop_settled(session_hash)
154
+ lapsed = None if entry else self._pop_lapsed(session_hash)
155
+ if lapsed:
156
+ return Access(mode="denied", nonce=lapsed, notice=embed_notice(EMBED_UNAVAILABLE_MESSAGE))
157
+ if entry is None or entry.mode is None:
158
+ notice = entry.notice if entry and entry.notice else EMBED_UNAVAILABLE_MESSAGE
159
+ return Access(mode="denied", notice=embed_notice(notice))
160
  return Access(nonce=entry.nonce)
161
 
162
+ def _pop_settled(self, session_hash: str) -> Optional[GrantEntry]:
163
+ settled = [entry for entry in self._session_entries(session_hash) if entry.settled_at is not None]
164
+ return self._entries.pop(min(settled, key=lambda entry: entry.settled_at).nonce) if settled else None
165
+
166
+ def _pop_lapsed(self, session_hash: str) -> Optional[str]:
167
+ lapsed = self._lapsed.get(session_hash)
168
+ if not lapsed:
169
+ return None
170
+ nonce = lapsed.pop(0)
171
+ if not lapsed:
172
+ del self._lapsed[session_hash]
173
+ return nonce
174
+
175
+ def _session_entries(self, session_hash: str) -> List[GrantEntry]:
176
+ return [entry for entry in self._entries.values() if entry.session_hash == session_hash]
177
+
178
  def _purge(self) -> None:
179
+ now = time.time()
180
+ for nonce, entry in list(self._entries.items()):
181
+ pending = entry.settled_at is None
182
+ started = entry.issued_at if pending else entry.settled_at
183
+ if started < now - (self._ttl if pending else self._settled_ttl):
184
+ del self._entries[nonce]
185
+ if entry.mode is not None:
186
+ self._lapse(entry)
187
+
188
+ def _lapse(self, entry: GrantEntry) -> None:
189
+ self._lapsed.setdefault(entry.session_hash, []).append(entry.nonce)
190
+ while len(self._lapsed) > self._max_entries:
191
+ del self._lapsed[next(iter(self._lapsed))]
192
 
193
 
194
  class PublicAllowance:
 
302
  self.grants = grants
303
  self.public = public
304
  self._notices: Dict[str, str] = {}
305
+ self._refunds: Dict[str, List[str]] = {}
306
  self._lock = threading.Lock()
307
 
308
  def authorize(self, request: Any, session_hash: str) -> Access:
309
  if grants_required(request):
310
+ access = self.grants.take(session_hash)
311
+ if access.mode == "denied":
312
+ self.generation_failed(session_hash, access)
313
+ return access
314
  if not (self.public.enabled and is_browser_visit(request)):
315
  return Access()
316
  key = self.public.key_for(request)
 
327
  self.public.refund(access.visitor_key)
328
  if access.nonce:
329
  with self._lock:
330
+ self._refunds.setdefault(session_hash, []).append(access.nonce)
331
 
332
  def refund_receipt(self, session_hash: str) -> Optional[Dict[str, str]]:
333
  with self._lock:
334
+ pending = self._refunds.get(session_hash) or []
335
+ nonce = pending.pop(0) if pending else None
336
+ if not pending:
337
+ self._refunds.pop(session_hash, None)
338
  ticket = issue_ticket(nonce, REFUND) if nonce else None
339
  return {"ticket": ticket} if ticket else None
340
 
quota_fallback.py CHANGED
@@ -208,6 +208,14 @@ def verify_permit(token: Any, receipt: Dict[str, Any]) -> Optional[Dict[str, Any
208
  return payload
209
 
210
 
 
 
 
 
 
 
 
 
211
  def verify_grant(token: Any, nonce: str) -> Optional[Dict[str, Any]]:
212
  payload = _open_document(token)
213
  if payload is None or payload.get("purpose") != GRANT_PURPOSE:
 
208
  return payload
209
 
210
 
211
+ def grant_request_nonce(token: Any) -> Optional[str]:
212
+ payload = _open_document(token)
213
+ if payload is None or payload.get("operation") != GRANT or payload.get("space") != space_identifier():
214
+ return None
215
+ nonce = payload.get("nonce")
216
+ return nonce if isinstance(nonce, str) else None
217
+
218
+
219
  def verify_grant(token: Any, nonce: str) -> Optional[Dict[str, Any]]:
220
  payload = _open_document(token)
221
  if payload is None or payload.get("purpose") != GRANT_PURPOSE:
tests/test_access_limits.py CHANGED
@@ -107,6 +107,57 @@ class GrantBookTests(unittest.TestCase):
107
  self.book.settle("session-a", {"ticket": receipt["ticket"], "grant": grant_for(nonce)}, receipt)
108
  self.assertEqual(self.book.take("session-a").mode, "denied")
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  class PublicAllowanceTests(unittest.TestCase):
112
  def setUp(self):
@@ -211,6 +262,29 @@ class AccessPolicyTests(unittest.TestCase):
211
  self.assertIsNone(qf.verify_receipt(refund["ticket"]))
212
  self.assertIsNone(self.policy.refund_receipt("e"))
213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  def test_notices_are_delivered_once(self):
215
  self.policy.remember("s", "<div>notice</div>")
216
  self.assertEqual(self.policy.pop_notice("s"), "<div>notice</div>")
 
107
  self.book.settle("session-a", {"ticket": receipt["ticket"], "grant": grant_for(nonce)}, receipt)
108
  self.assertEqual(self.book.take("session-a").mode, "denied")
109
 
110
+ def test_grant_requests_are_bounded_per_session_and_in_total(self):
111
+ for _ in range(al.MAX_GRANTS_PER_SESSION):
112
+ self.assertIsNotNone(self.book.request("session-a"))
113
+ self.assertIsNone(self.book.request("session-a"))
114
+ self.assertIsNotNone(self.book.request("session-b"))
115
+ book = al.GrantBook(max_entries=1)
116
+ self.assertIsNotNone(book.request("one"))
117
+ self.assertIsNone(book.request("two"))
118
+
119
+ def settle(self, receipt, nonce, session="session-a"):
120
+ self.book.settle(session, {"ticket": receipt["ticket"], "grant": grant_for(nonce)}, receipt)
121
+
122
+ def test_a_settled_grant_survives_a_long_gpu_queue(self):
123
+ receipt, nonce = self.issue()
124
+ self.settle(receipt, nonce)
125
+ with mock.patch.object(al.time, "time", return_value=time.time() + al.GRANT_TTL_SECONDS + 60):
126
+ access = self.book.take("session-a")
127
+ self.assertEqual((access.mode, access.nonce), ("native", nonce))
128
+
129
+ def test_a_lapsed_grant_is_denied_with_its_nonce_for_a_refund(self):
130
+ receipt, nonce = self.issue()
131
+ self.settle(receipt, nonce)
132
+ with mock.patch.object(al.time, "time", return_value=time.time() + al.SETTLED_GRANT_TTL_SECONDS + 1):
133
+ access = self.book.take("session-a")
134
+ self.assertEqual((access.mode, access.nonce), ("denied", nonce))
135
+ self.assertIsNone(self.book.take("session-a").nonce)
136
+
137
+ def test_overlapping_runs_in_one_session_each_keep_their_grant(self):
138
+ first, first_nonce = self.issue()
139
+ second, second_nonce = self.issue()
140
+ self.settle(first, first_nonce)
141
+ self.settle(second, second_nonce)
142
+ self.assertEqual(self.book.take("session-a").nonce, first_nonce)
143
+ self.assertEqual(self.book.take("session-a").nonce, second_nonce)
144
+ self.assertEqual(self.book.take("session-a").mode, "denied")
145
+
146
+ def test_overlapping_runs_keep_their_own_refusals(self):
147
+ first, first_nonce = self.issue()
148
+ second, _nonce = self.issue()
149
+ self.settle(first, first_nonce)
150
+ self.book.settle("session-a", {"ticket": second["ticket"], "reason": al.FREE_LIMIT_REASON, "error": ""}, second)
151
+ self.assertEqual(self.book.take("session-a").nonce, first_nonce)
152
+ self.assertIn(al.EMBED_LIMIT_MESSAGE, self.book.take("session-a").notice)
153
+
154
+ def test_an_ack_for_an_unknown_nonce_marks_no_other_grant(self):
155
+ receipt, nonce = self.issue()
156
+ stranger = {"ticket": qf.issue_ticket("e" * 32, qf.GRANT)}
157
+ self.book.settle("session-a", {"ticket": stranger["ticket"], "grant": grant_for("e" * 32)}, stranger)
158
+ self.settle(receipt, nonce)
159
+ self.assertEqual(self.book.take("session-a").nonce, nonce)
160
+
161
 
162
  class PublicAllowanceTests(unittest.TestCase):
163
  def setUp(self):
 
262
  self.assertIsNone(qf.verify_receipt(refund["ticket"]))
263
  self.assertIsNone(self.policy.refund_receipt("e"))
264
 
265
+ def grant(self, session="e"):
266
+ receipt = self.policy.grants.request(session)
267
+ nonce = opened(receipt["ticket"])["nonce"]
268
+ self.policy.grants.settle(session, {"ticket": receipt["ticket"], "grant": grant_for(nonce)}, receipt)
269
+ return nonce
270
+
271
+ def test_overlapping_failed_generations_are_each_refunded(self):
272
+ with mock.patch.dict(os.environ, {"LABS_EMBED_GRANTS": "1"}):
273
+ nonces = [self.grant(), self.grant()]
274
+ for _ in nonces:
275
+ access = self.policy.authorize(embed_request(), "e")
276
+ self.assertEqual(access.mode, "native")
277
+ self.policy.generation_failed("e", access)
278
+ self.assertEqual([opened(self.policy.refund_receipt("e")["ticket"])["nonce"] for _ in nonces], nonces)
279
+ self.assertIsNone(self.policy.refund_receipt("e"))
280
+
281
+ def test_a_lapsed_grant_is_refunded_when_its_run_is_denied(self):
282
+ with mock.patch.dict(os.environ, {"LABS_EMBED_GRANTS": "1"}):
283
+ nonce = self.grant()
284
+ with mock.patch.object(al.time, "time", return_value=time.time() + al.SETTLED_GRANT_TTL_SECONDS + 1):
285
+ self.assertEqual(self.policy.authorize(embed_request(), "e").mode, "denied")
286
+ self.assertEqual(opened(self.policy.refund_receipt("e")["ticket"])["nonce"], nonce)
287
+
288
  def test_notices_are_delivered_once(self):
289
  self.policy.remember("s", "<div>notice</div>")
290
  self.assertEqual(self.policy.pop_notice("s"), "<div>notice</div>")