HongzeFu commited on
Commit
4ac0c53
·
1 Parent(s): 7c2aaf6
gradio-web/config.py CHANGED
@@ -22,8 +22,6 @@ UI_GLOBAL_FONT_SIZE = "24px"
22
  SESSION_TIMEOUT = 20 # 30秒无用户主动操作后,交由 gr.State TTL 自动回收 session
23
  SESSION_CONCURRENCY_ID = "session_actions"
24
  SESSION_CONCURRENCY_LIMIT = 2
25
- SESSION_INIT_CONCURRENCY_ID = "session_init"
26
- SESSION_INIT_CONCURRENCY_LIMIT = 2
27
 
28
  # 兜底执行次数配置
29
  EXECUTE_LIMIT_OFFSET = 4 # 兜底执行次数 = non_demonstration_task_length + EXECUTE_LIMIT_OFFSET
@@ -89,6 +87,7 @@ UI_TEXT = {
89
  "progress": {
90
  "episode_loading": "The episode is loading...",
91
  "queue_wait": "Lots of people are playing! Please wait...",
 
92
  },
93
  "errors": {
94
  "load_missing_task": "Error loading task: missing current_task",
 
22
  SESSION_TIMEOUT = 20 # 30秒无用户主动操作后,交由 gr.State TTL 自动回收 session
23
  SESSION_CONCURRENCY_ID = "session_actions"
24
  SESSION_CONCURRENCY_LIMIT = 2
 
 
25
 
26
  # 兜底执行次数配置
27
  EXECUTE_LIMIT_OFFSET = 4 # 兜底执行次数 = non_demonstration_task_length + EXECUTE_LIMIT_OFFSET
 
87
  "progress": {
88
  "episode_loading": "The episode is loading...",
89
  "queue_wait": "Lots of people are playing! Please wait...",
90
+ "entry_rejected": "too many people playing",
91
  },
92
  "errors": {
93
  "load_missing_task": "Error loading task: missing current_task",
gradio-web/gradio_callbacks.py CHANGED
@@ -16,7 +16,6 @@ from PIL import Image
16
 
17
  from state_manager import (
18
  cleanup_session,
19
- create_session,
20
  get_execute_count,
21
  get_play_button_clicked,
22
  get_session,
@@ -71,18 +70,15 @@ def _session_error_text():
71
  return _ui_text("log", "session_error")
72
 
73
 
 
 
 
 
74
  def touch_session(uid):
75
  """Re-emit the current session key to refresh gr.State TTL."""
76
  return uid if uid and get_session(uid) is not None else None
77
 
78
 
79
- def touch_session_or_preserve_pending(uid, init_pending=False):
80
- """Keep pending init uid alive until a real session is created."""
81
- if init_pending:
82
- return uid
83
- return touch_session(uid)
84
-
85
-
86
  def cleanup_user_session(uid):
87
  """Unified cleanup entry for gr.State TTL deletion and unload hooks."""
88
  if not uid:
@@ -692,8 +688,8 @@ def init_session_and_load_task(uid):
692
  if not uid:
693
  return _task_load_failed_response(uid, _session_error_text())
694
 
695
- if get_session(uid) is None:
696
- create_session(uid)
697
 
698
  return _load_initialized_session_task(uid)
699
 
@@ -716,7 +712,7 @@ def _load_initialized_session_task(uid):
716
 
717
 
718
  def try_init_session_and_load_task(uid):
719
- """Try to initialize the session without blocking on a full slot queue."""
720
  if not uid:
721
  return {
722
  "status": "ready",
@@ -724,17 +720,18 @@ def try_init_session_and_load_task(uid):
724
  }
725
 
726
  if get_session(uid) is None:
727
- ready, queue_position = try_create_session(uid)
728
  if not ready:
 
729
  LOGGER.info(
730
- "try_init_session_and_load_task pending uid=%s queue_position=%s",
731
  _uid_for_log(uid),
732
- queue_position,
733
  )
734
  return {
735
- "status": "pending",
736
  "uid": uid,
737
- "queue_position": queue_position,
 
738
  }
739
 
740
  return {
@@ -743,14 +740,6 @@ def try_init_session_and_load_task(uid):
743
  }
744
 
745
 
746
- def resume_pending_init(uid, init_pending=False, request: gr.Request | None = None):
747
- """Retry a previously pending init attempt."""
748
- if not init_pending:
749
- return {"status": "skip"}
750
- effective_uid = uid or getattr(request, "session_hash", None)
751
- return try_init_session_and_load_task(effective_uid)
752
-
753
-
754
  def load_next_task_wrapper(uid):
755
  """Move to a random episode within the same env and reload task."""
756
  if not uid or get_session(uid) is None:
 
16
 
17
  from state_manager import (
18
  cleanup_session,
 
19
  get_execute_count,
20
  get_play_button_clicked,
21
  get_session,
 
70
  return _ui_text("log", "session_error")
71
 
72
 
73
+ def _entry_rejected_text():
74
+ return _ui_text("progress", "entry_rejected")
75
+
76
+
77
  def touch_session(uid):
78
  """Re-emit the current session key to refresh gr.State TTL."""
79
  return uid if uid and get_session(uid) is not None else None
80
 
81
 
 
 
 
 
 
 
 
82
  def cleanup_user_session(uid):
83
  """Unified cleanup entry for gr.State TTL deletion and unload hooks."""
84
  if not uid:
 
688
  if not uid:
689
  return _task_load_failed_response(uid, _session_error_text())
690
 
691
+ if get_session(uid) is None and not try_create_session(uid):
692
+ return _task_load_failed_response(uid, _entry_rejected_text())
693
 
694
  return _load_initialized_session_task(uid)
695
 
 
712
 
713
 
714
  def try_init_session_and_load_task(uid):
715
+ """Try to initialize the session and reject immediately when full."""
716
  if not uid:
717
  return {
718
  "status": "ready",
 
720
  }
721
 
722
  if get_session(uid) is None:
723
+ ready = try_create_session(uid)
724
  if not ready:
725
+ message = _entry_rejected_text()
726
  LOGGER.info(
727
+ "try_init_session_and_load_task rejected uid=%s",
728
  _uid_for_log(uid),
 
729
  )
730
  return {
731
+ "status": "rejected",
732
  "uid": uid,
733
+ "message": message,
734
+ "load_result": _task_load_failed_response(uid, message),
735
  }
736
 
737
  return {
 
740
  }
741
 
742
 
 
 
 
 
 
 
 
 
743
  def load_next_task_wrapper(uid):
744
  """Move to a random episode within the same env and reload task."""
745
  if not uid or get_session(uid) is None:
gradio-web/state_manager.py CHANGED
@@ -16,7 +16,6 @@ LOGGER = logging.getLogger("robomme.state_manager")
16
  # --- 全局会话存储 ---
17
  GLOBAL_SESSIONS = {}
18
  ACTIVE_SESSION_SLOTS = set()
19
- WAITING_SESSION_QUEUE = []
20
 
21
  # --- 任务索引存储(用于进度显示) ---
22
  TASK_INDEX_MAP = {} # {uid: {"task_index": int, "total_tasks": int}}
@@ -34,7 +33,6 @@ TASK_START_TIMES = {} # {"{uid}:{env_id}:{episode_idx}": iso_timestamp}
34
  PLAY_BUTTON_CLICKED = {} # {uid: bool}
35
 
36
  _state_lock = threading.Lock()
37
- _session_slot_condition = threading.Condition(_state_lock)
38
 
39
 
40
  def get_session(uid):
@@ -43,70 +41,26 @@ def get_session(uid):
43
  return GLOBAL_SESSIONS.get(uid)
44
 
45
 
46
- def _cleanup_waiting_session_queue_locked():
47
- if not WAITING_SESSION_QUEUE:
48
- return
49
-
50
- cleaned_queue = []
51
- seen = set()
52
- for queued_uid in WAITING_SESSION_QUEUE:
53
- if not queued_uid or queued_uid in seen:
54
- continue
55
- if queued_uid in ACTIVE_SESSION_SLOTS:
56
- continue
57
- if queued_uid in GLOBAL_SESSIONS:
58
- continue
59
- seen.add(queued_uid)
60
- cleaned_queue.append(queued_uid)
61
-
62
- WAITING_SESSION_QUEUE[:] = cleaned_queue
63
-
64
-
65
- def _ensure_waiting_session_locked(uid):
66
- _cleanup_waiting_session_queue_locked()
67
- if uid not in WAITING_SESSION_QUEUE:
68
- WAITING_SESSION_QUEUE.append(uid)
69
- return WAITING_SESSION_QUEUE.index(uid) + 1
70
-
71
-
72
  def _try_reserve_session_slot_locked(uid, session_concurrency_limit):
73
  if uid in ACTIVE_SESSION_SLOTS:
74
- return True, 0
75
 
76
- _cleanup_waiting_session_queue_locked()
77
-
78
- if len(ACTIVE_SESSION_SLOTS) < int(session_concurrency_limit):
79
- if WAITING_SESSION_QUEUE and WAITING_SESSION_QUEUE[0] != uid:
80
- queue_position = _ensure_waiting_session_locked(uid)
81
- LOGGER.debug(
82
- "try_reserve_session_slot delayed uid=%s queue_position=%s active_slots=%s limit=%s",
83
- uid,
84
- queue_position,
85
- len(ACTIVE_SESSION_SLOTS),
86
- session_concurrency_limit,
87
- )
88
- return False, queue_position
89
-
90
- if WAITING_SESSION_QUEUE and WAITING_SESSION_QUEUE[0] == uid:
91
- WAITING_SESSION_QUEUE.pop(0)
92
-
93
- ACTIVE_SESSION_SLOTS.add(uid)
94
  LOGGER.info(
95
- "try_reserve_session_slot acquired uid=%s active_slots=%s",
96
  uid,
97
  len(ACTIVE_SESSION_SLOTS),
 
98
  )
99
- return True, 0
100
 
101
- queue_position = _ensure_waiting_session_locked(uid)
102
  LOGGER.info(
103
- "try_reserve_session_slot queued uid=%s queue_position=%s active_slots=%s limit=%s",
104
  uid,
105
- queue_position,
106
  len(ACTIVE_SESSION_SLOTS),
107
- session_concurrency_limit,
108
  )
109
- return False, queue_position
110
 
111
 
112
  def try_reserve_session_slot(uid):
@@ -114,57 +68,29 @@ def try_reserve_session_slot(uid):
114
  Try to reserve a session slot without blocking.
115
 
116
  Returns:
117
- tuple[bool, int]: (acquired, queue_position)
118
  """
119
  if not uid:
120
  raise ValueError("Session uid cannot be empty")
121
 
122
  from config import SESSION_CONCURRENCY_LIMIT
123
 
124
- with _session_slot_condition:
125
  return _try_reserve_session_slot_locked(uid, SESSION_CONCURRENCY_LIMIT)
126
 
127
 
128
- def reserve_session_slot(uid):
129
- """
130
- Block until a session slot is available for this uid.
131
-
132
- Slots are held for the full lifetime of a ProcessSessionProxy and released
133
- only after cleanup closes the worker process.
134
- """
135
- if not uid:
136
- raise ValueError("Session uid cannot be empty")
137
-
138
- from config import SESSION_CONCURRENCY_LIMIT
139
-
140
- with _session_slot_condition:
141
- while True:
142
- acquired, queue_position = _try_reserve_session_slot_locked(uid, SESSION_CONCURRENCY_LIMIT)
143
- if acquired:
144
- return
145
- LOGGER.info(
146
- "reserve_session_slot waiting uid=%s queue_position=%s active_slots=%s",
147
- uid,
148
- queue_position,
149
- len(ACTIVE_SESSION_SLOTS),
150
- )
151
- _session_slot_condition.wait(timeout=0.1)
152
-
153
-
154
  def release_session_slot(uid):
155
  if not uid:
156
  return
157
 
158
- with _session_slot_condition:
159
  if uid in ACTIVE_SESSION_SLOTS:
160
  ACTIVE_SESSION_SLOTS.remove(uid)
161
- _cleanup_waiting_session_queue_locked()
162
  LOGGER.info(
163
  "release_session_slot uid=%s active_slots=%s",
164
  uid,
165
  len(ACTIVE_SESSION_SLOTS),
166
  )
167
- _session_slot_condition.notify_all()
168
 
169
 
170
  def try_create_session(uid):
@@ -172,56 +98,48 @@ def try_create_session(uid):
172
  Try to create a ProcessSessionProxy without blocking on session slot wait.
173
 
174
  Returns:
175
- tuple[bool, int]: (ready, queue_position)
176
  """
177
  if not uid:
178
  raise ValueError("Session uid cannot be empty")
179
 
180
  with _state_lock:
181
  if GLOBAL_SESSIONS.get(uid) is not None:
182
- return True, 0
183
-
184
- acquired, queue_position = try_reserve_session_slot(uid)
185
- if not acquired:
186
- return False, queue_position
187
-
188
- try:
189
- with _state_lock:
190
- session = GLOBAL_SESSIONS.get(uid)
191
- if session is None:
192
- session = ProcessSessionProxy()
193
- GLOBAL_SESSIONS[uid] = session
194
- except Exception:
195
- release_session_slot(uid)
196
- raise
197
 
198
  LOGGER.info("try_create_session uid=%s total_sessions=%s", uid, len(GLOBAL_SESSIONS))
199
- return True, 0
200
 
201
 
202
  def create_session(uid):
203
  """
204
  为指定 session key 创建 ProcessSessionProxy。
205
 
206
- 只在初始页面 load 调用uid 由 request.session_hash 提供
207
  """
208
  if not uid:
209
  raise ValueError("Session uid cannot be empty")
210
 
211
- reserve_session_slot(uid)
212
- try:
213
- with _state_lock:
214
- session = GLOBAL_SESSIONS.get(uid)
215
- if session is None:
216
- session = ProcessSessionProxy()
217
- GLOBAL_SESSIONS[uid] = session
218
- except Exception:
219
- release_session_slot(uid)
220
- raise
221
  LOGGER.info("create_session uid=%s total_sessions=%s", uid, len(GLOBAL_SESSIONS))
222
  return uid
223
 
224
 
 
 
 
 
 
 
225
  def get_task_index(uid):
226
  """获取任务索引信息。"""
227
  with _state_lock:
@@ -331,8 +249,6 @@ def cleanup_session(uid):
331
 
332
  with _state_lock:
333
  session = GLOBAL_SESSIONS.pop(uid, None)
334
- if uid in WAITING_SESSION_QUEUE:
335
- WAITING_SESSION_QUEUE[:] = [queued_uid for queued_uid in WAITING_SESSION_QUEUE if queued_uid != uid]
336
  TASK_INDEX_MAP.pop(uid, None)
337
  UI_PHASE_MAP.pop(uid, None)
338
  PLAY_BUTTON_CLICKED.pop(uid, None)
 
16
  # --- 全局会话存储 ---
17
  GLOBAL_SESSIONS = {}
18
  ACTIVE_SESSION_SLOTS = set()
 
19
 
20
  # --- 任务索引存储(用于进度显示) ---
21
  TASK_INDEX_MAP = {} # {uid: {"task_index": int, "total_tasks": int}}
 
33
  PLAY_BUTTON_CLICKED = {} # {uid: bool}
34
 
35
  _state_lock = threading.Lock()
 
36
 
37
 
38
  def get_session(uid):
 
41
  return GLOBAL_SESSIONS.get(uid)
42
 
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  def _try_reserve_session_slot_locked(uid, session_concurrency_limit):
45
  if uid in ACTIVE_SESSION_SLOTS:
46
+ return True
47
 
48
+ if len(ACTIVE_SESSION_SLOTS) >= int(session_concurrency_limit):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  LOGGER.info(
50
+ "try_reserve_session_slot rejected uid=%s active_slots=%s limit=%s",
51
  uid,
52
  len(ACTIVE_SESSION_SLOTS),
53
+ session_concurrency_limit,
54
  )
55
+ return False
56
 
57
+ ACTIVE_SESSION_SLOTS.add(uid)
58
  LOGGER.info(
59
+ "try_reserve_session_slot acquired uid=%s active_slots=%s",
60
  uid,
 
61
  len(ACTIVE_SESSION_SLOTS),
 
62
  )
63
+ return True
64
 
65
 
66
  def try_reserve_session_slot(uid):
 
68
  Try to reserve a session slot without blocking.
69
 
70
  Returns:
71
+ bool: whether the slot was acquired
72
  """
73
  if not uid:
74
  raise ValueError("Session uid cannot be empty")
75
 
76
  from config import SESSION_CONCURRENCY_LIMIT
77
 
78
+ with _state_lock:
79
  return _try_reserve_session_slot_locked(uid, SESSION_CONCURRENCY_LIMIT)
80
 
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  def release_session_slot(uid):
83
  if not uid:
84
  return
85
 
86
+ with _state_lock:
87
  if uid in ACTIVE_SESSION_SLOTS:
88
  ACTIVE_SESSION_SLOTS.remove(uid)
 
89
  LOGGER.info(
90
  "release_session_slot uid=%s active_slots=%s",
91
  uid,
92
  len(ACTIVE_SESSION_SLOTS),
93
  )
 
94
 
95
 
96
  def try_create_session(uid):
 
98
  Try to create a ProcessSessionProxy without blocking on session slot wait.
99
 
100
  Returns:
101
+ bool: whether the session is ready
102
  """
103
  if not uid:
104
  raise ValueError("Session uid cannot be empty")
105
 
106
  with _state_lock:
107
  if GLOBAL_SESSIONS.get(uid) is not None:
108
+ return True
109
+ if not _try_reserve_session_slot_locked(uid, _get_session_concurrency_limit()):
110
+ return False
111
+ try:
112
+ GLOBAL_SESSIONS[uid] = ProcessSessionProxy()
113
+ except Exception:
114
+ ACTIVE_SESSION_SLOTS.discard(uid)
115
+ raise
 
 
 
 
 
 
 
116
 
117
  LOGGER.info("try_create_session uid=%s total_sessions=%s", uid, len(GLOBAL_SESSIONS))
118
+ return True
119
 
120
 
121
  def create_session(uid):
122
  """
123
  为指定 session key 创建 ProcessSessionProxy。
124
 
125
+ 超出并发上限立即失败不执行排队等待
126
  """
127
  if not uid:
128
  raise ValueError("Session uid cannot be empty")
129
 
130
+ ready = try_create_session(uid)
131
+ if not ready:
132
+ raise RuntimeError("No session slots available")
 
 
 
 
 
 
 
133
  LOGGER.info("create_session uid=%s total_sessions=%s", uid, len(GLOBAL_SESSIONS))
134
  return uid
135
 
136
 
137
+ def _get_session_concurrency_limit():
138
+ from config import SESSION_CONCURRENCY_LIMIT
139
+
140
+ return SESSION_CONCURRENCY_LIMIT
141
+
142
+
143
  def get_task_index(uid):
144
  """获取任务索引信息。"""
145
  with _state_lock:
 
249
 
250
  with _state_lock:
251
  session = GLOBAL_SESSIONS.pop(uid, None)
 
 
252
  TASK_INDEX_MAP.pop(uid, None)
253
  UI_PHASE_MAP.pop(uid, None)
254
  PLAY_BUTTON_CLICKED.pop(uid, None)
gradio-web/test/test_queue_session_limit_e2e.py CHANGED
@@ -185,17 +185,46 @@ def _mount_demo(demo):
185
  return root_url, demo, server, thread
186
 
187
 
188
- def test_gradio_queue_respects_configured_limit_on_init_load(monkeypatch):
189
  config = importlib.reload(importlib.import_module("config"))
190
- importlib.reload(importlib.import_module("gradio_callbacks"))
 
191
  ui_layout = importlib.reload(importlib.import_module("ui_layout"))
192
 
193
- def fake_init_app(request):
194
- uid = str(getattr(request, "session_hash", "missing"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  time.sleep(6.0)
196
- return _minimal_load_result(uid, log_text=f"ready:{uid}")
 
 
 
 
197
 
198
- monkeypatch.setattr(ui_layout, "init_app", fake_init_app)
 
 
 
199
 
200
  demo = ui_layout.create_ui_blocks()
201
  root_url, demo, server, thread = _mount_demo(demo)
@@ -204,53 +233,45 @@ def test_gradio_queue_respects_configured_limit_on_init_load(monkeypatch):
204
  with sync_playwright() as p:
205
  browser = p.chromium.launch(headless=True)
206
  pages = []
207
- total_pages = int(config.SESSION_INIT_CONCURRENCY_LIMIT) + 2
208
  for _ in range(total_pages):
209
  page = browser.new_page(viewport={"width": 1280, "height": 900})
210
  page.goto(root_url, wait_until="domcontentloaded")
211
  pages.append(page)
212
  time.sleep(0.25)
213
 
214
- def _queue_snapshot_ready():
215
- progress_texts = [_read_unified_overlay_text(page) for page in pages]
216
- processing_ready = all(
217
- text and config.UI_TEXT["progress"]["episode_loading"] in text
218
- for text in progress_texts[: config.SESSION_INIT_CONCURRENCY_LIMIT]
219
  )
220
- queued_ready = all(
221
- text
222
- and config.UI_TEXT["progress"]["queue_wait"] in text
223
- and "queue:" in text.lower()
224
- for text in progress_texts[config.SESSION_INIT_CONCURRENCY_LIMIT :]
225
- )
226
- return processing_ready and queued_ready
227
 
228
- _wait_until(_queue_snapshot_ready, timeout_s=10.0)
229
  processing_pages = [
230
  _read_unified_overlay_text(page) or ""
231
- for page in pages[: config.SESSION_INIT_CONCURRENCY_LIMIT]
232
  ]
233
- queued_pages = [
234
  _read_unified_overlay_text(page) or ""
235
- for page in pages[config.SESSION_INIT_CONCURRENCY_LIMIT :]
236
  ]
237
 
238
- assert all(config.UI_TEXT["progress"]["episode_loading"] in text for text in processing_pages)
239
- assert all(
240
- config.UI_TEXT["progress"]["queue_wait"] in text and "queue:" in text.lower()
241
- for text in queued_pages
242
- )
243
  assert pages[0].evaluate("() => !!document.getElementById('loading_overlay_group')") is False
244
  overlay_snapshot = _read_progress_overlay_snapshot(pages[-1])
245
  assert overlay_snapshot["present"] is True
246
  assert overlay_snapshot["width"] and overlay_snapshot["width"] > 0
247
  assert overlay_snapshot["height"] and overlay_snapshot["height"] >= 400
248
  assert overlay_snapshot["background"] == "rgba(255, 255, 255, 0.92)"
249
- session_wait_snapshot = _read_session_wait_overlay_snapshot(pages[-1])
250
- assert session_wait_snapshot["content"] in (None, "")
251
-
252
- _wait_until(lambda: _read_progress_text(pages[0]) is None, timeout_s=15.0)
253
- _wait_until(lambda: _read_progress_text(pages[-1]) is None, timeout_s=25.0)
254
 
255
  browser.close()
256
  finally:
@@ -302,6 +323,7 @@ def test_gradio_state_ttl_cleans_up_idle_session(monkeypatch):
302
  page.goto(root_url, wait_until="domcontentloaded")
303
  page.wait_for_selector("#main_interface_root", state="visible", timeout=15000)
304
 
 
305
  uid = next(iter(state_manager.GLOBAL_SESSIONS))
306
  assert uid in user_manager_mod.user_manager.session_progress
307
 
@@ -341,7 +363,7 @@ def test_single_load_uses_native_episode_loading_copy(monkeypatch):
341
  page.goto(root_url, wait_until="domcontentloaded")
342
 
343
  _wait_until(
344
- lambda: (_read_progress_text(page) or "").startswith(config.UI_TEXT["progress"]["episode_loading"]),
345
  timeout_s=8.0,
346
  )
347
  assert page.evaluate("() => !!document.getElementById('loading_overlay_group')") is False
@@ -352,7 +374,6 @@ def test_single_load_uses_native_episode_loading_copy(monkeypatch):
352
  assert overlay_snapshot["background"] == "rgba(255, 255, 255, 0.92)"
353
 
354
  page.wait_for_selector("#main_interface_root", state="visible", timeout=15000)
355
- _wait_until(lambda: _read_progress_text(page) is None, timeout_s=8.0)
356
 
357
  browser.close()
358
  finally:
@@ -426,7 +447,7 @@ def test_execute_does_not_use_episode_loading_copy(monkeypatch):
426
 
427
  body_text = page.evaluate("() => document.body.innerText")
428
  assert "The episode is loading..." not in body_text
429
- assert "Lots of people are playing! Please wait..." not in body_text
430
 
431
  browser.close()
432
  finally:
@@ -435,7 +456,7 @@ def test_execute_does_not_use_episode_loading_copy(monkeypatch):
435
  demo.close()
436
 
437
 
438
- def test_late_user_waits_for_active_session_slot_release(monkeypatch):
439
  config = importlib.reload(importlib.import_module("config"))
440
  state_manager = importlib.reload(importlib.import_module("state_manager"))
441
  callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
@@ -508,18 +529,20 @@ def test_late_user_waits_for_active_session_slot_release(monkeypatch):
508
  assert len(state_manager.GLOBAL_SESSIONS) == config.SESSION_CONCURRENCY_LIMIT
509
  assert len(state_manager.ACTIVE_SESSION_SLOTS) == config.SESSION_CONCURRENCY_LIMIT
510
  assert _read_session_wait_overlay_snapshot(page3)["visible"] is True
511
- wait_snapshot = _read_session_wait_overlay_snapshot(page3)
512
- assert wait_snapshot["content"] == f'{config.UI_TEXT["progress"]["queue_wait"]} | queue: 1'
513
- assert wait_snapshot["proseBackground"] == "rgba(0, 0, 0, 0)"
514
- assert wait_snapshot["proseBorderRadius"] == "0px"
515
- assert wait_snapshot["proseBoxShadow"] == "none"
516
 
517
  page1.close()
518
 
519
  _wait_until(lambda: len(closed) >= 1, timeout_s=10.0)
520
- _wait_until(lambda: _read_session_wait_overlay_snapshot(page3)["visible"] is False, timeout_s=15.0)
521
- _wait_until(lambda: len(state_manager.GLOBAL_SESSIONS) == config.SESSION_CONCURRENCY_LIMIT, timeout_s=10.0)
522
- _wait_until(lambda: len(state_manager.ACTIVE_SESSION_SLOTS) == config.SESSION_CONCURRENCY_LIMIT, timeout_s=10.0)
 
 
523
 
524
  browser.close()
525
  finally:
@@ -528,7 +551,7 @@ def test_late_user_waits_for_active_session_slot_release(monkeypatch):
528
  demo.close()
529
 
530
 
531
- def test_second_and_later_late_users_show_queue_overlay(monkeypatch):
532
  config = importlib.reload(importlib.import_module("config"))
533
  state_manager = importlib.reload(importlib.import_module("state_manager"))
534
  callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
@@ -582,8 +605,11 @@ def test_second_and_later_late_users_show_queue_overlay(monkeypatch):
582
  active_pages.append(page)
583
  time.sleep(0.25)
584
 
585
- for page in active_pages:
586
- _wait_until(lambda page=page: _read_progress_text(page) is None, timeout_s=15.0)
 
 
 
587
 
588
  waiting_page = browser.new_page(viewport={"width": 1280, "height": 900})
589
  waiting_page.goto(root_url, wait_until="domcontentloaded")
@@ -595,21 +621,21 @@ def test_second_and_later_late_users_show_queue_overlay(monkeypatch):
595
  queued_pages.append(page)
596
  time.sleep(0.25)
597
 
598
- pending_pages = [waiting_page, *queued_pages]
599
 
600
- def _pending_pages_ready():
601
- snapshots = [_read_session_wait_overlay_snapshot(page) for page in pending_pages]
602
- return all(snapshot["visible"] is True for snapshot in snapshots)
603
 
604
- _wait_until(_pending_pages_ready, timeout_s=10.0)
 
605
 
606
- for page in pending_pages:
607
  overlay_snapshot = _read_session_wait_overlay_snapshot(page)
608
  assert overlay_snapshot["present"] is True
609
  assert overlay_snapshot["width"] and overlay_snapshot["width"] > 0
610
  assert overlay_snapshot["height"] and overlay_snapshot["height"] >= 400
611
- assert config.UI_TEXT["progress"]["queue_wait"] in str(overlay_snapshot["content"] or "")
612
- assert "queue:" in str(overlay_snapshot["content"] or "").lower()
613
  assert overlay_snapshot["proseBackground"] == "rgba(0, 0, 0, 0)"
614
  assert overlay_snapshot["proseBorderRadius"] == "0px"
615
  assert overlay_snapshot["proseBoxShadow"] == "none"
@@ -621,7 +647,7 @@ def test_second_and_later_late_users_show_queue_overlay(monkeypatch):
621
  demo.close()
622
 
623
 
624
- def test_active_user_execute_is_not_blocked_by_waiting_init_loads(monkeypatch):
625
  config = importlib.reload(importlib.import_module("config"))
626
  state_manager = importlib.reload(importlib.import_module("state_manager"))
627
  callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
@@ -682,11 +708,17 @@ def test_active_user_execute_is_not_blocked_by_waiting_init_loads(monkeypatch):
682
 
683
  page1 = browser.new_page(viewport={"width": 1280, "height": 900})
684
  page1.goto(root_url, wait_until="domcontentloaded")
685
- _wait_until(lambda: _read_progress_text(page1) is None, timeout_s=15.0)
 
 
 
686
 
687
  page2 = browser.new_page(viewport={"width": 1280, "height": 900})
688
  page2.goto(root_url, wait_until="domcontentloaded")
689
- _wait_until(lambda: _read_progress_text(page2) is None, timeout_s=15.0)
 
 
 
690
 
691
  waiting_page = browser.new_page(viewport={"width": 1280, "height": 900})
692
  waiting_page.goto(root_url, wait_until="domcontentloaded")
@@ -705,10 +737,11 @@ def test_active_user_execute_is_not_blocked_by_waiting_init_loads(monkeypatch):
705
  timeout_s=6.0,
706
  )
707
  page1_progress = _read_progress_text(page1) or ""
708
- assert "queue:" not in page1_progress.lower()
709
- assert config.UI_TEXT["progress"]["queue_wait"] not in page1_progress
710
  assert _read_session_wait_overlay_snapshot(waiting_page)["visible"] is True
711
  assert _read_session_wait_overlay_snapshot(queued_page)["visible"] is True
 
 
712
 
713
  browser.close()
714
  finally:
@@ -717,7 +750,7 @@ def test_active_user_execute_is_not_blocked_by_waiting_init_loads(monkeypatch):
717
  demo.close()
718
 
719
 
720
- def test_waiting_page_enters_task_after_idle_session_ttl_release(monkeypatch):
721
  config = importlib.reload(importlib.import_module("config"))
722
  state_manager = importlib.reload(importlib.import_module("state_manager"))
723
  callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
@@ -773,29 +806,33 @@ def test_waiting_page_enters_task_after_idle_session_ttl_release(monkeypatch):
773
  active_pages.append(page)
774
  time.sleep(0.25)
775
 
776
- for page in active_pages:
777
- _wait_until(lambda page=page: _read_progress_text(page) is None, timeout_s=15.0)
 
 
 
778
 
779
- waiting_page = browser.new_page(viewport={"width": 1280, "height": 900})
780
- waiting_page.goto(root_url, wait_until="domcontentloaded")
781
 
782
  _wait_until(
783
- lambda: _read_session_wait_overlay_snapshot(waiting_page)["visible"] is True,
784
  timeout_s=10.0,
785
  )
786
  _wait_until(
787
  lambda: len(state_manager.ACTIVE_SESSION_SLOTS) < config.SESSION_CONCURRENCY_LIMIT,
788
  timeout_s=10.0,
789
  )
 
 
 
 
 
790
  _wait_until(
791
- lambda: _read_session_wait_overlay_snapshot(waiting_page)["visible"] is False,
792
- timeout_s=15.0,
793
- )
794
- _wait_until(
795
- lambda: (_read_log_output_value(waiting_page) or "").strip() == "Please select the action.\nActions with 🎯 need to select a point on the image as input",
796
  timeout_s=15.0,
797
  )
798
- assert _read_progress_text(waiting_page) is None
799
 
800
  browser.close()
801
  finally:
 
185
  return root_url, demo, server, thread
186
 
187
 
188
+ def test_entry_rejects_immediately_when_session_limit_is_full(monkeypatch):
189
  config = importlib.reload(importlib.import_module("config"))
190
+ state_manager = importlib.reload(importlib.import_module("state_manager"))
191
+ callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
192
  ui_layout = importlib.reload(importlib.import_module("ui_layout"))
193
 
194
+ class _FakeProxy:
195
+ def __init__(self):
196
+ self.env_id = None
197
+ self.episode_idx = None
198
+ self.language_goal = "goal"
199
+ self.available_options = [("pick", 0)]
200
+ self.raw_solve_options = [{"label": "a", "action": "pick", "available": False}]
201
+ self.demonstration_frames = []
202
+
203
+ def load_episode(self, env_id, episode_idx):
204
+ self.env_id = env_id
205
+ self.episode_idx = episode_idx
206
+ return Image.new("RGB", (32, 32), color=(10, 20, 30)), "loaded"
207
+
208
+ def get_pil_image(self, use_segmented=False):
209
+ _ = use_segmented
210
+ return Image.new("RGB", (32, 32), color=(10, 20, 30))
211
+
212
+ def close(self):
213
+ return None
214
+
215
+ def fake_init_session(uid):
216
+ _ = uid
217
  time.sleep(6.0)
218
+ return (
219
+ True,
220
+ "ok",
221
+ {"current_task": {"env_id": "BinFill", "episode_idx": 1}, "completed_count": 0},
222
+ )
223
 
224
+ monkeypatch.setattr(state_manager, "ProcessSessionProxy", _FakeProxy)
225
+ monkeypatch.setattr(callbacks.user_manager, "init_session", fake_init_session)
226
+ monkeypatch.setattr(callbacks, "get_task_hint", lambda env_id: "")
227
+ monkeypatch.setattr(callbacks, "should_show_demo_video", lambda env_id: False)
228
 
229
  demo = ui_layout.create_ui_blocks()
230
  root_url, demo, server, thread = _mount_demo(demo)
 
233
  with sync_playwright() as p:
234
  browser = p.chromium.launch(headless=True)
235
  pages = []
236
+ total_pages = int(config.SESSION_CONCURRENCY_LIMIT) + 2
237
  for _ in range(total_pages):
238
  page = browser.new_page(viewport={"width": 1280, "height": 900})
239
  page.goto(root_url, wait_until="domcontentloaded")
240
  pages.append(page)
241
  time.sleep(0.25)
242
 
243
+ def _overlay_snapshot_ready():
244
+ overlay_texts = [_read_unified_overlay_text(page) for page in pages]
245
+ rejected_ready = all(
246
+ text == config.UI_TEXT["progress"]["entry_rejected"]
247
+ for text in overlay_texts[config.SESSION_CONCURRENCY_LIMIT :]
248
  )
249
+ return rejected_ready
 
 
 
 
 
 
250
 
251
+ _wait_until(_overlay_snapshot_ready, timeout_s=10.0)
252
  processing_pages = [
253
  _read_unified_overlay_text(page) or ""
254
+ for page in pages[: config.SESSION_CONCURRENCY_LIMIT]
255
  ]
256
+ rejected_pages = [
257
  _read_unified_overlay_text(page) or ""
258
+ for page in pages[config.SESSION_CONCURRENCY_LIMIT :]
259
  ]
260
 
261
+ assert all(text != config.UI_TEXT["progress"]["entry_rejected"] for text in processing_pages)
262
+ assert all(text == config.UI_TEXT["progress"]["entry_rejected"] for text in rejected_pages)
263
+ assert len(state_manager.GLOBAL_SESSIONS) == config.SESSION_CONCURRENCY_LIMIT
264
+ assert len(state_manager.ACTIVE_SESSION_SLOTS) == config.SESSION_CONCURRENCY_LIMIT
 
265
  assert pages[0].evaluate("() => !!document.getElementById('loading_overlay_group')") is False
266
  overlay_snapshot = _read_progress_overlay_snapshot(pages[-1])
267
  assert overlay_snapshot["present"] is True
268
  assert overlay_snapshot["width"] and overlay_snapshot["width"] > 0
269
  assert overlay_snapshot["height"] and overlay_snapshot["height"] >= 400
270
  assert overlay_snapshot["background"] == "rgba(255, 255, 255, 0.92)"
271
+ rejection_snapshot = _read_session_wait_overlay_snapshot(pages[-1])
272
+ assert rejection_snapshot["content"] == config.UI_TEXT["progress"]["entry_rejected"]
273
+ assert _read_unified_overlay_text(pages[-1]) == config.UI_TEXT["progress"]["entry_rejected"]
274
+ assert len(state_manager.GLOBAL_SESSIONS) == config.SESSION_CONCURRENCY_LIMIT
 
275
 
276
  browser.close()
277
  finally:
 
323
  page.goto(root_url, wait_until="domcontentloaded")
324
  page.wait_for_selector("#main_interface_root", state="visible", timeout=15000)
325
 
326
+ _wait_until(lambda: len(state_manager.GLOBAL_SESSIONS) == 1, timeout_s=8.0)
327
  uid = next(iter(state_manager.GLOBAL_SESSIONS))
328
  assert uid in user_manager_mod.user_manager.session_progress
329
 
 
363
  page.goto(root_url, wait_until="domcontentloaded")
364
 
365
  _wait_until(
366
+ lambda: _read_progress_overlay_snapshot(page)["present"] is True,
367
  timeout_s=8.0,
368
  )
369
  assert page.evaluate("() => !!document.getElementById('loading_overlay_group')") is False
 
374
  assert overlay_snapshot["background"] == "rgba(255, 255, 255, 0.92)"
375
 
376
  page.wait_for_selector("#main_interface_root", state="visible", timeout=15000)
 
377
 
378
  browser.close()
379
  finally:
 
447
 
448
  body_text = page.evaluate("() => document.body.innerText")
449
  assert "The episode is loading..." not in body_text
450
+ assert "too many people playing" not in body_text
451
 
452
  browser.close()
453
  finally:
 
456
  demo.close()
457
 
458
 
459
+ def test_rejected_user_stays_rejected_after_active_session_slot_release(monkeypatch):
460
  config = importlib.reload(importlib.import_module("config"))
461
  state_manager = importlib.reload(importlib.import_module("state_manager"))
462
  callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
 
529
  assert len(state_manager.GLOBAL_SESSIONS) == config.SESSION_CONCURRENCY_LIMIT
530
  assert len(state_manager.ACTIVE_SESSION_SLOTS) == config.SESSION_CONCURRENCY_LIMIT
531
  assert _read_session_wait_overlay_snapshot(page3)["visible"] is True
532
+ reject_snapshot = _read_session_wait_overlay_snapshot(page3)
533
+ assert reject_snapshot["content"] == config.UI_TEXT["progress"]["entry_rejected"]
534
+ assert reject_snapshot["proseBackground"] == "rgba(0, 0, 0, 0)"
535
+ assert reject_snapshot["proseBorderRadius"] == "0px"
536
+ assert reject_snapshot["proseBoxShadow"] == "none"
537
 
538
  page1.close()
539
 
540
  _wait_until(lambda: len(closed) >= 1, timeout_s=10.0)
541
+ _wait_until(lambda: len(state_manager.GLOBAL_SESSIONS) == config.SESSION_CONCURRENCY_LIMIT - 1, timeout_s=10.0)
542
+ _wait_until(lambda: len(state_manager.ACTIVE_SESSION_SLOTS) == config.SESSION_CONCURRENCY_LIMIT - 1, timeout_s=10.0)
543
+ time.sleep(1.0)
544
+ assert _read_session_wait_overlay_snapshot(page3)["visible"] is True
545
+ assert _read_unified_overlay_text(page3) == config.UI_TEXT["progress"]["entry_rejected"]
546
 
547
  browser.close()
548
  finally:
 
551
  demo.close()
552
 
553
 
554
+ def test_second_and_later_late_users_show_rejected_overlay(monkeypatch):
555
  config = importlib.reload(importlib.import_module("config"))
556
  state_manager = importlib.reload(importlib.import_module("state_manager"))
557
  callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
 
605
  active_pages.append(page)
606
  time.sleep(0.25)
607
 
608
+ _wait_until(
609
+ lambda: len(state_manager.GLOBAL_SESSIONS) == config.SESSION_CONCURRENCY_LIMIT
610
+ and len(state_manager.ACTIVE_SESSION_SLOTS) == config.SESSION_CONCURRENCY_LIMIT,
611
+ timeout_s=15.0,
612
+ )
613
 
614
  waiting_page = browser.new_page(viewport={"width": 1280, "height": 900})
615
  waiting_page.goto(root_url, wait_until="domcontentloaded")
 
621
  queued_pages.append(page)
622
  time.sleep(0.25)
623
 
624
+ rejected_pages = [waiting_page, *queued_pages]
625
 
626
+ def _rejected_pages_ready():
627
+ texts = [_read_unified_overlay_text(page) for page in rejected_pages]
628
+ return all(text == config.UI_TEXT["progress"]["entry_rejected"] for text in texts)
629
 
630
+ _wait_until(_rejected_pages_ready, timeout_s=10.0)
631
+ assert len(state_manager.GLOBAL_SESSIONS) == config.SESSION_CONCURRENCY_LIMIT
632
 
633
+ for page in rejected_pages:
634
  overlay_snapshot = _read_session_wait_overlay_snapshot(page)
635
  assert overlay_snapshot["present"] is True
636
  assert overlay_snapshot["width"] and overlay_snapshot["width"] > 0
637
  assert overlay_snapshot["height"] and overlay_snapshot["height"] >= 400
638
+ assert str(overlay_snapshot["content"] or "") == config.UI_TEXT["progress"]["entry_rejected"]
 
639
  assert overlay_snapshot["proseBackground"] == "rgba(0, 0, 0, 0)"
640
  assert overlay_snapshot["proseBorderRadius"] == "0px"
641
  assert overlay_snapshot["proseBoxShadow"] == "none"
 
647
  demo.close()
648
 
649
 
650
+ def test_active_user_execute_is_not_blocked_by_rejected_init_loads(monkeypatch):
651
  config = importlib.reload(importlib.import_module("config"))
652
  state_manager = importlib.reload(importlib.import_module("state_manager"))
653
  callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
 
708
 
709
  page1 = browser.new_page(viewport={"width": 1280, "height": 900})
710
  page1.goto(root_url, wait_until="domcontentloaded")
711
+ _wait_until(
712
+ lambda: (_read_log_output_value(page1) or "").startswith("Please select the action."),
713
+ timeout_s=15.0,
714
+ )
715
 
716
  page2 = browser.new_page(viewport={"width": 1280, "height": 900})
717
  page2.goto(root_url, wait_until="domcontentloaded")
718
+ _wait_until(
719
+ lambda: (_read_log_output_value(page2) or "").startswith("Please select the action."),
720
+ timeout_s=15.0,
721
+ )
722
 
723
  waiting_page = browser.new_page(viewport={"width": 1280, "height": 900})
724
  waiting_page.goto(root_url, wait_until="domcontentloaded")
 
737
  timeout_s=6.0,
738
  )
739
  page1_progress = _read_progress_text(page1) or ""
740
+ assert config.UI_TEXT["progress"]["entry_rejected"] not in page1_progress
 
741
  assert _read_session_wait_overlay_snapshot(waiting_page)["visible"] is True
742
  assert _read_session_wait_overlay_snapshot(queued_page)["visible"] is True
743
+ assert _read_unified_overlay_text(waiting_page) == config.UI_TEXT["progress"]["entry_rejected"]
744
+ assert _read_unified_overlay_text(queued_page) == config.UI_TEXT["progress"]["entry_rejected"]
745
 
746
  browser.close()
747
  finally:
 
750
  demo.close()
751
 
752
 
753
+ def test_rejected_page_requires_refresh_after_idle_session_ttl_release(monkeypatch):
754
  config = importlib.reload(importlib.import_module("config"))
755
  state_manager = importlib.reload(importlib.import_module("state_manager"))
756
  callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
 
806
  active_pages.append(page)
807
  time.sleep(0.25)
808
 
809
+ _wait_until(
810
+ lambda: len(state_manager.GLOBAL_SESSIONS) == config.SESSION_CONCURRENCY_LIMIT
811
+ and len(state_manager.ACTIVE_SESSION_SLOTS) == config.SESSION_CONCURRENCY_LIMIT,
812
+ timeout_s=15.0,
813
+ )
814
 
815
+ rejected_page = browser.new_page(viewport={"width": 1280, "height": 900})
816
+ rejected_page.goto(root_url, wait_until="domcontentloaded")
817
 
818
  _wait_until(
819
+ lambda: _read_session_wait_overlay_snapshot(rejected_page)["visible"] is True,
820
  timeout_s=10.0,
821
  )
822
  _wait_until(
823
  lambda: len(state_manager.ACTIVE_SESSION_SLOTS) < config.SESSION_CONCURRENCY_LIMIT,
824
  timeout_s=10.0,
825
  )
826
+ time.sleep(1.0)
827
+ assert _read_session_wait_overlay_snapshot(rejected_page)["visible"] is True
828
+ assert _read_unified_overlay_text(rejected_page) == config.UI_TEXT["progress"]["entry_rejected"]
829
+
830
+ rejected_page.reload(wait_until="domcontentloaded")
831
  _wait_until(
832
+ lambda: (_read_log_output_value(rejected_page) or "").strip() == "Please select the action.\nActions with 🎯 need to select a point on the image as input",
 
 
 
 
833
  timeout_s=15.0,
834
  )
835
+ assert _read_session_wait_overlay_snapshot(rejected_page)["visible"] is False
836
 
837
  browser.close()
838
  finally:
gradio-web/test/test_ui_native_layout_contract.py CHANGED
@@ -169,7 +169,7 @@ def test_native_ui_config_contains_phase_machine_and_precheck_chain(reload_modul
169
  )
170
  assert all("Loading environment, please wait..." not in str(v) for v in values)
171
  assert "The episode is loading..." in ui_layout.PROGRESS_TEXT_REWRITE_JS
172
- assert "Lots of people are playing! Please wait..." in ui_layout.PROGRESS_TEXT_REWRITE_JS
173
 
174
  log_output_comp = next(
175
  comp
 
169
  )
170
  assert all("Loading environment, please wait..." not in str(v) for v in values)
171
  assert "The episode is loading..." in ui_layout.PROGRESS_TEXT_REWRITE_JS
172
+ assert ui_layout.UI_TEXT["progress"]["entry_rejected"] == "too many people playing"
173
 
174
  log_output_comp = next(
175
  comp
gradio-web/ui_layout.py CHANGED
@@ -15,8 +15,6 @@ from config import (
15
  LIVE_OBS_POINT_WAIT_CLASS,
16
  SESSION_CONCURRENCY_ID,
17
  SESSION_CONCURRENCY_LIMIT,
18
- SESSION_INIT_CONCURRENCY_ID,
19
- SESSION_INIT_CONCURRENCY_LIMIT,
20
  SESSION_TIMEOUT,
21
  LIVE_OBS_REFRESH_HZ,
22
  POINT_SELECTION_SCALE,
@@ -39,13 +37,11 @@ from gradio_callbacks import (
39
  on_video_end_transition,
40
  precheck_execute_inputs,
41
  refresh_live_obs,
42
- resume_pending_init,
43
  restart_episode_wrapper,
44
  switch_env_wrapper,
45
  switch_to_action_phase,
46
  switch_to_execute_phase,
47
  touch_session,
48
- touch_session_or_preserve_pending,
49
  )
50
  from user_manager import user_manager
51
 
@@ -715,9 +711,15 @@ def _with_phase_from_load(load_result):
715
  return (
716
  *load_result,
717
  phase,
718
- False,
719
  gr.update(value=""),
720
- gr.update(active=False),
 
 
 
 
 
 
 
721
  )
722
 
723
 
@@ -787,11 +789,9 @@ def create_ui_blocks():
787
  delete_callback=cleanup_user_session,
788
  )
789
  ui_phase_state = gr.State(value=PHASE_INIT)
790
- session_boot_pending_state = gr.State(value=False)
791
  current_task_env_state = gr.State(value=None)
792
  suppress_next_option_change_state = gr.State(value=False)
793
  live_obs_timer = gr.Timer(value=1.0 / LIVE_OBS_REFRESH_HZ, active=True)
794
- session_init_retry_timer = gr.Timer(value=0.5, active=False)
795
 
796
  task_info_box = gr.Textbox(visible=False, elem_id="task_info_box")
797
  progress_info_box = gr.Textbox(visible=False)
@@ -799,7 +799,7 @@ def create_ui_blocks():
799
 
800
  with gr.Column(visible=True, elem_id="main_interface_root") as main_interface:
801
  native_progress_host = gr.Markdown(
802
- value="",
803
  visible=True,
804
  container=False,
805
  elem_id="native_progress_host",
@@ -939,9 +939,7 @@ def create_ui_blocks():
939
  task_hint_display,
940
  reference_action_btn,
941
  ui_phase_state,
942
- session_boot_pending_state,
943
  native_progress_host,
944
- session_init_retry_timer,
945
  ]
946
  phase_visibility_outputs = [
947
  video_phase_group,
@@ -952,51 +950,20 @@ def create_ui_blocks():
952
  "concurrency_id": SESSION_CONCURRENCY_ID,
953
  "concurrency_limit": SESSION_CONCURRENCY_LIMIT,
954
  }
955
- init_queue_kwargs = {
956
- "concurrency_id": SESSION_INIT_CONCURRENCY_ID,
957
- "concurrency_limit": SESSION_INIT_CONCURRENCY_LIMIT,
958
- }
959
 
960
  def _skip_load_flow():
961
  return tuple(gr.skip() for _ in range(len(load_flow_outputs)))
962
 
963
- def _pending_init_flow(uid, queue_position):
964
- queue_label = f'queue: {max(1, int(queue_position or 1))}'
965
- return (
966
- uid,
967
- gr.update(visible=True),
968
- gr.update(interactive=False),
969
- "",
970
- gr.update(choices=[], value=None),
971
- "",
972
- "",
973
- gr.update(value=None, visible=False),
974
- gr.update(visible=False, interactive=False),
975
- "",
976
- "",
977
- gr.update(interactive=False),
978
- gr.update(interactive=False),
979
- gr.update(interactive=False),
980
- gr.update(visible=False),
981
- gr.update(visible=False),
982
- gr.update(visible=False),
983
- gr.update(value=""),
984
- gr.update(interactive=False),
985
- PHASE_INIT,
986
- True,
987
- gr.update(value=f'{UI_TEXT["progress"]["queue_wait"]} | {queue_label}'),
988
- gr.update(active=True),
989
- )
990
-
991
  def _coerce_init_load_result(result):
992
  if isinstance(result, dict):
993
  status = result.get("status")
994
- if status == "pending":
995
- return _pending_init_flow(result.get("uid"), result.get("queue_position"))
996
- if status == "skip":
997
- return _skip_load_flow()
998
  if status == "ready":
999
  return _with_phase_from_load(result.get("load_result"))
 
 
 
 
 
1000
  return _with_phase_from_load(result)
1001
 
1002
  def _normalize_env_choice(env_value, choices):
@@ -1043,9 +1010,6 @@ def create_ui_blocks():
1043
  def init_app_with_phase(request: gr.Request):
1044
  return _coerce_init_load_result(init_app(request))
1045
 
1046
- def resume_pending_init_with_phase(uid, init_pending, request: gr.Request):
1047
- return _coerce_init_load_result(resume_pending_init(uid, init_pending, request))
1048
-
1049
  def load_next_task_with_phase(uid):
1050
  return _with_phase_from_load(load_next_task_wrapper(uid))
1051
 
@@ -1410,7 +1374,7 @@ def create_ui_blocks():
1410
  show_progress="full",
1411
  js=SET_EPISODE_LOAD_MODE_JS,
1412
  show_progress_on=[native_progress_host],
1413
- **init_queue_kwargs,
1414
  ).then(
1415
  fn=_phase_visibility_updates,
1416
  inputs=[ui_phase_state],
@@ -1424,8 +1388,8 @@ def create_ui_blocks():
1424
  queue=False,
1425
  show_progress="hidden",
1426
  ).then(
1427
- fn=touch_session_or_preserve_pending,
1428
- inputs=[uid_state, session_boot_pending_state],
1429
  outputs=[uid_state],
1430
  queue=False,
1431
  show_progress="hidden",
@@ -1443,32 +1407,6 @@ def create_ui_blocks():
1443
  show_progress="hidden",
1444
  )
1445
 
1446
- session_init_retry_timer.tick(
1447
- fn=resume_pending_init_with_phase,
1448
- inputs=[uid_state, session_boot_pending_state],
1449
- outputs=load_flow_outputs,
1450
- show_progress="hidden",
1451
- **init_queue_kwargs,
1452
- ).then(
1453
- fn=_phase_visibility_updates,
1454
- inputs=[ui_phase_state],
1455
- outputs=phase_visibility_outputs,
1456
- queue=False,
1457
- show_progress="hidden",
1458
- ).then(
1459
- fn=sync_header_from_task,
1460
- inputs=[task_info_box, goal_box],
1461
- outputs=[header_task_box, header_goal_box, current_task_env_state],
1462
- queue=False,
1463
- show_progress="hidden",
1464
- ).then(
1465
- fn=touch_session_or_preserve_pending,
1466
- inputs=[uid_state, session_boot_pending_state],
1467
- outputs=[uid_state],
1468
- queue=False,
1469
- show_progress="hidden",
1470
- )
1471
-
1472
  demo.unload(fn=cleanup_current_request_session)
1473
  demo.queue(max_size=None, default_concurrency_limit=None)
1474
 
 
15
  LIVE_OBS_POINT_WAIT_CLASS,
16
  SESSION_CONCURRENCY_ID,
17
  SESSION_CONCURRENCY_LIMIT,
 
 
18
  SESSION_TIMEOUT,
19
  LIVE_OBS_REFRESH_HZ,
20
  POINT_SELECTION_SCALE,
 
37
  on_video_end_transition,
38
  precheck_execute_inputs,
39
  refresh_live_obs,
 
40
  restart_episode_wrapper,
41
  switch_env_wrapper,
42
  switch_to_action_phase,
43
  switch_to_execute_phase,
44
  touch_session,
 
45
  )
46
  from user_manager import user_manager
47
 
 
711
  return (
712
  *load_result,
713
  phase,
 
714
  gr.update(value=""),
715
+ )
716
+
717
+
718
+ def _with_rejected_init(load_result, message):
719
+ return (
720
+ *load_result,
721
+ PHASE_INIT,
722
+ gr.update(value=message),
723
  )
724
 
725
 
 
789
  delete_callback=cleanup_user_session,
790
  )
791
  ui_phase_state = gr.State(value=PHASE_INIT)
 
792
  current_task_env_state = gr.State(value=None)
793
  suppress_next_option_change_state = gr.State(value=False)
794
  live_obs_timer = gr.Timer(value=1.0 / LIVE_OBS_REFRESH_HZ, active=True)
 
795
 
796
  task_info_box = gr.Textbox(visible=False, elem_id="task_info_box")
797
  progress_info_box = gr.Textbox(visible=False)
 
799
 
800
  with gr.Column(visible=True, elem_id="main_interface_root") as main_interface:
801
  native_progress_host = gr.Markdown(
802
+ value=UI_TEXT["progress"]["episode_loading"],
803
  visible=True,
804
  container=False,
805
  elem_id="native_progress_host",
 
939
  task_hint_display,
940
  reference_action_btn,
941
  ui_phase_state,
 
942
  native_progress_host,
 
943
  ]
944
  phase_visibility_outputs = [
945
  video_phase_group,
 
950
  "concurrency_id": SESSION_CONCURRENCY_ID,
951
  "concurrency_limit": SESSION_CONCURRENCY_LIMIT,
952
  }
 
 
 
 
953
 
954
  def _skip_load_flow():
955
  return tuple(gr.skip() for _ in range(len(load_flow_outputs)))
956
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
957
  def _coerce_init_load_result(result):
958
  if isinstance(result, dict):
959
  status = result.get("status")
 
 
 
 
960
  if status == "ready":
961
  return _with_phase_from_load(result.get("load_result"))
962
+ if status == "rejected":
963
+ return _with_rejected_init(
964
+ result.get("load_result"),
965
+ result.get("message", UI_TEXT["progress"]["entry_rejected"]),
966
+ )
967
  return _with_phase_from_load(result)
968
 
969
  def _normalize_env_choice(env_value, choices):
 
1010
  def init_app_with_phase(request: gr.Request):
1011
  return _coerce_init_load_result(init_app(request))
1012
 
 
 
 
1013
  def load_next_task_with_phase(uid):
1014
  return _with_phase_from_load(load_next_task_wrapper(uid))
1015
 
 
1374
  show_progress="full",
1375
  js=SET_EPISODE_LOAD_MODE_JS,
1376
  show_progress_on=[native_progress_host],
1377
+ queue=False,
1378
  ).then(
1379
  fn=_phase_visibility_updates,
1380
  inputs=[ui_phase_state],
 
1388
  queue=False,
1389
  show_progress="hidden",
1390
  ).then(
1391
+ fn=touch_session,
1392
+ inputs=[uid_state],
1393
  outputs=[uid_state],
1394
  queue=False,
1395
  show_progress="hidden",
 
1407
  show_progress="hidden",
1408
  )
1409
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1410
  demo.unload(fn=cleanup_current_request_session)
1411
  demo.queue(max_size=None, default_concurrency_limit=None)
1412