youssefreda9 commited on
Commit
c15c00a
·
1 Parent(s): cfddf82

fix: wake sleeping HF Space before Gradio Client connection

Browse files

- Add _wake_sleeping_space() helper that checks Space runtime status via
HF API and polls until RUNNING before connecting
- Add 'could not fetch config' to retryable error keywords
- Increase retries from 3 to 5 and delays from 2-8s to 10-30s to handle
cold starts (~30-90s)
- Mark 'could not fetch config' as transient to prevent permanent caching

Fixes ValueError: Could not fetch config when Space is sleeping on
free cpu-basic hardware (48h inactivity timeout)

Files changed (1) hide show
  1. src/nlp/grammar/grammar_service.py +84 -8
src/nlp/grammar/grammar_service.py CHANGED
@@ -136,6 +136,78 @@ class GrammarChecker:
136
  return text
137
 
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  def get_grammar_model():
140
  """
141
  Lazy-load the grammar model on first call.
@@ -163,11 +235,14 @@ def get_grammar_model():
163
  t0 = time.time()
164
  logger.info("Loading Grammar model (lazy init)...")
165
 
166
- # 1. Initialize Gradio Client with retry for rate limiting / sleeping Spaces
 
 
 
167
  # HF_TOKEN is already set in the environment (module-level) so Client picks it up automatically
168
  from gradio_client import Client
169
  client = None
170
- max_retries = 3
171
  last_err = None
172
 
173
  for attempt in range(1, max_retries + 1):
@@ -181,10 +256,11 @@ def get_grammar_model():
181
  err_msg = str(conn_err).lower()
182
  is_retryable = any(kw in err_msg for kw in [
183
  'too many requests', 'rate limit', '429',
184
- 'timeout', 'connection', 'sleeping'
 
185
  ])
186
  if is_retryable and attempt < max_retries:
187
- wait = 2 ** attempt # 2s, 4s, 8s
188
  logger.warning(
189
  f"Gradio connection attempt {attempt} failed ({conn_err}). "
190
  f"Retrying in {wait}s..."
@@ -196,13 +272,13 @@ def get_grammar_model():
196
  if client is None:
197
  raise RuntimeError(f"Gradio connection failed after {max_retries} attempts: {last_err}")
198
 
199
- # 2. Initialize rule-based post-processor (camel-tools)
200
  logger.info("Loading ArabicGrammarGuard (camel-tools MLE disambiguator)...")
201
  from nlp.grammar.grammar_rules import ArabicGrammarGuard
202
  rules = ArabicGrammarGuard()
203
  logger.info("ArabicGrammarGuard loaded")
204
 
205
- # 3. Create GrammarChecker instance
206
  _grammar_checker = GrammarChecker(client, rules)
207
 
208
  elapsed = time.time() - t0
@@ -215,11 +291,11 @@ def get_grammar_model():
215
  logger.error(f"Failed to load grammar model: {e}")
216
  logger.error(traceback.format_exc())
217
 
218
- # Transient errors (rate limiting, network) should NOT be cached —
219
  # allow retry on next request
220
  transient_keywords = ['Too many requests', 'rate limit', 'timeout',
221
  'ConnectionError', 'ConnectTimeout', 'ReadTimeout',
222
- '429', 'sleeping']
223
  is_transient = any(kw.lower() in error_msg.lower() for kw in transient_keywords)
224
 
225
  if is_transient:
 
136
  return text
137
 
138
 
139
+ def _wake_sleeping_space(space_id: str, timeout: int = 120, poll_interval: int = 5):
140
+ """
141
+ Check if a HF Space is sleeping and wake it up before connecting.
142
+
143
+ Free cpu-basic Spaces sleep after 48h of inactivity. The Gradio Client
144
+ can't fetch the config while the container is booting, so we proactively
145
+ wake it and wait until the runtime stage becomes RUNNING.
146
+
147
+ Args:
148
+ space_id: The HuggingFace Space ID (e.g. "user/space-name").
149
+ timeout: Maximum seconds to wait for the Space to wake up.
150
+ poll_interval: Seconds between status polls.
151
+ """
152
+ try:
153
+ import requests as _requests
154
+
155
+ hf_token = os.environ.get("HF_TOKEN", "").strip()
156
+ headers = {}
157
+ if hf_token:
158
+ headers["Authorization"] = f"Bearer {hf_token}"
159
+
160
+ api_url = f"https://huggingface.co/api/spaces/{space_id}"
161
+
162
+ # Check current runtime stage
163
+ resp = _requests.get(api_url, headers=headers, timeout=15)
164
+ resp.raise_for_status()
165
+ info = resp.json()
166
+ stage = info.get("runtime", {}).get("stage", "UNKNOWN")
167
+ logger.info(f"Space '{space_id}' runtime stage: {stage}")
168
+
169
+ if stage == "RUNNING":
170
+ return # Already running, nothing to do
171
+
172
+ if stage in ("SLEEPING", "PAUSED", "STOPPED"):
173
+ # Hit the Space URL to trigger a wake-up
174
+ host = info.get("host", f"https://{space_id.replace('/', '-')}.hf.space")
175
+ logger.info(f"Space is {stage} — sending wake-up request to {host}")
176
+ try:
177
+ _requests.get(host, headers=headers, timeout=10)
178
+ except Exception:
179
+ pass # The request itself may timeout; that's fine — it triggers the wake
180
+
181
+ # Poll until RUNNING or timeout
182
+ start = time.time()
183
+ while time.time() - start < timeout:
184
+ time.sleep(poll_interval)
185
+ try:
186
+ resp = _requests.get(api_url, headers=headers, timeout=15)
187
+ resp.raise_for_status()
188
+ stage = resp.json().get("runtime", {}).get("stage", "UNKNOWN")
189
+ logger.info(f"Space '{space_id}' stage: {stage} (waited {time.time() - start:.0f}s)")
190
+ if stage == "RUNNING":
191
+ logger.info(f"Space '{space_id}' is now RUNNING")
192
+ return
193
+ if stage in ("BUILD_ERROR", "RUNTIME_ERROR", "CONFIG_ERROR"):
194
+ logger.error(f"Space '{space_id}' entered error stage: {stage}")
195
+ return # Let the Gradio Client handle the error
196
+ except Exception as poll_err:
197
+ logger.warning(f"Error polling Space status: {poll_err}")
198
+
199
+ logger.warning(
200
+ f"Space '{space_id}' did not reach RUNNING within {timeout}s "
201
+ f"(last stage: {stage}). Proceeding anyway..."
202
+ )
203
+ else:
204
+ logger.info(f"Space stage is '{stage}' — proceeding with Gradio Client connection")
205
+
206
+ except Exception as e:
207
+ # Non-critical: if we can't check/wake, fall through to normal Gradio retry logic
208
+ logger.warning(f"Could not check/wake Space '{space_id}': {e}")
209
+
210
+
211
  def get_grammar_model():
212
  """
213
  Lazy-load the grammar model on first call.
 
235
  t0 = time.time()
236
  logger.info("Loading Grammar model (lazy init)...")
237
 
238
+ # 1. Wake the Space if it is sleeping (free cpu-basic Spaces sleep after 48h)
239
+ _wake_sleeping_space(GRADIO_SPACE)
240
+
241
+ # 2. Initialize Gradio Client — with retry for cold-start / rate limiting
242
  # HF_TOKEN is already set in the environment (module-level) so Client picks it up automatically
243
  from gradio_client import Client
244
  client = None
245
+ max_retries = 5
246
  last_err = None
247
 
248
  for attempt in range(1, max_retries + 1):
 
256
  err_msg = str(conn_err).lower()
257
  is_retryable = any(kw in err_msg for kw in [
258
  'too many requests', 'rate limit', '429',
259
+ 'timeout', 'connection', 'sleeping',
260
+ 'could not fetch config',
261
  ])
262
  if is_retryable and attempt < max_retries:
263
+ wait = min(10 * attempt, 30) # 10s, 20s, 30s, 30s
264
  logger.warning(
265
  f"Gradio connection attempt {attempt} failed ({conn_err}). "
266
  f"Retrying in {wait}s..."
 
272
  if client is None:
273
  raise RuntimeError(f"Gradio connection failed after {max_retries} attempts: {last_err}")
274
 
275
+ # 3. Initialize rule-based post-processor (camel-tools)
276
  logger.info("Loading ArabicGrammarGuard (camel-tools MLE disambiguator)...")
277
  from nlp.grammar.grammar_rules import ArabicGrammarGuard
278
  rules = ArabicGrammarGuard()
279
  logger.info("ArabicGrammarGuard loaded")
280
 
281
+ # 4. Create GrammarChecker instance
282
  _grammar_checker = GrammarChecker(client, rules)
283
 
284
  elapsed = time.time() - t0
 
291
  logger.error(f"Failed to load grammar model: {e}")
292
  logger.error(traceback.format_exc())
293
 
294
+ # Transient errors (rate limiting, network, sleeping) should NOT be cached —
295
  # allow retry on next request
296
  transient_keywords = ['Too many requests', 'rate limit', 'timeout',
297
  'ConnectionError', 'ConnectTimeout', 'ReadTimeout',
298
+ '429', 'sleeping', 'could not fetch config']
299
  is_transient = any(kw.lower() in error_msg.lower() for kw in transient_keywords)
300
 
301
  if is_transient: