gcal/gmail: update MCP servers + SKALD.md
This commit is contained in:
@@ -36,6 +36,24 @@ def log(msg: str) -> None:
|
||||
# Protects all stdout writes (main thread + poll thread).
|
||||
_stdout_lock = threading.Lock()
|
||||
|
||||
# Serializes every Calendar API call and the service/credential build.
|
||||
#
|
||||
# googleapiclient is built on httplib2, which is NOT thread-safe: a service object
|
||||
# owns a single Http holding one reused TLS connection. The poll thread and the
|
||||
# request thread sharing that object means two threads can drive the same OpenSSL
|
||||
# socket at once — which does not raise, it segfaults (this exact race killed the
|
||||
# Gmail connector in production with SIGSEGV, no traceback). `_creds` has the same
|
||||
# problem: google.auth refreshes it in place, so two concurrent refreshes corrupt
|
||||
# the token.
|
||||
#
|
||||
# Every API call in this file goes through `_call`, and the service and creds are
|
||||
# built in `_get_service`, so serializing those two entry points covers the whole
|
||||
# surface — including any call site added later. Reentrant because the two can nest.
|
||||
#
|
||||
# Held per-call, never per-batch: correctness needs non-concurrency on the socket,
|
||||
# not atomicity, and a poll tick must not wait for a long listing to finish.
|
||||
_api_lock = threading.RLock()
|
||||
|
||||
# ── Push notifications ─────────────────────────────────────────────────────────
|
||||
|
||||
def _emit_notification(method: str, params: dict) -> None:
|
||||
@@ -215,9 +233,12 @@ def _build_service() -> Any:
|
||||
|
||||
def _get_service() -> Any:
|
||||
global _service
|
||||
if _service is None:
|
||||
_service = _build_service()
|
||||
return _service
|
||||
# Locked: two threads racing here would build (and refresh) two services, and
|
||||
# the loser's would be dropped while its connection was still in flight.
|
||||
with _api_lock:
|
||||
if _service is None:
|
||||
_service = _build_service()
|
||||
return _service
|
||||
|
||||
|
||||
# ── Error mapping & refresh-on-auth-error ──────────────────────────────────────
|
||||
@@ -245,21 +266,25 @@ def _call(fn: Callable[[], Any], api_label: str) -> Any:
|
||||
or a RefreshError. We refresh once, persist the new token, and retry the call.
|
||||
Anything else (or a second failure) is re-raised so the caller can format it
|
||||
via _format_google_error.
|
||||
|
||||
Holds `_api_lock` for the whole body — see the lock's comment. This is the one
|
||||
choke point every `.execute()` in this file passes through.
|
||||
"""
|
||||
try:
|
||||
return fn()
|
||||
except Exception as e:
|
||||
if not _is_auth_error(e) or _creds is None or not getattr(_creds, "refresh_token", None):
|
||||
raise
|
||||
with _api_lock:
|
||||
try:
|
||||
from google.auth.transport.requests import Request
|
||||
_creds.refresh(Request())
|
||||
_persist_creds()
|
||||
log("Access token refreshed mid-session after auth error; retrying the call.")
|
||||
except Exception as refresh_err:
|
||||
log(f"Mid-session token refresh failed: {refresh_err}")
|
||||
raise
|
||||
return fn()
|
||||
return fn()
|
||||
except Exception as e:
|
||||
if not _is_auth_error(e) or _creds is None or not getattr(_creds, "refresh_token", None):
|
||||
raise
|
||||
try:
|
||||
from google.auth.transport.requests import Request
|
||||
_creds.refresh(Request())
|
||||
_persist_creds()
|
||||
log("Access token refreshed mid-session after auth error; retrying the call.")
|
||||
except Exception as refresh_err:
|
||||
log(f"Mid-session token refresh failed: {refresh_err}")
|
||||
raise
|
||||
return fn()
|
||||
|
||||
|
||||
def _http_error_reason(e: Exception) -> str:
|
||||
|
||||
Reference in New Issue
Block a user