Skip to content

Use Isolated Execution Scopes

An Isolated Execution Scope provides a dedicated, per-connection state for running WATCH/MULTI/EXEC transactions. GLIDE clients normally multiplex commands over shared connections, which means per-connection state like WATCH cannot be held safely for a single caller. A scope gives you an exclusive connection where your WATCH keys remain valid until you call EXEC.

Use isolated scopes when you need optimistic concurrency control (OCC) — for example, incrementing a counter only if no other client modified it, or implementing compare-and-swap patterns.

from glide import GlideClient
client = await GlideClient.create(config)
async with await client.scoped_connection(routing_key="my-key") as scope:
# Use scope for WATCH/MULTI/EXEC
pass

The classic optimistic concurrency control pattern works as follows:

  1. Acquire a scope
  2. WATCH the key(s) you intend to modify
  3. Read the current value
  4. Begin a MULTI transaction
  5. Write the new value
  6. EXEC — returns null if any watched key was modified by another client
  7. Retry if EXEC returned null
from glide import GlideClient
client = await GlideClient.create(config)
max_retries = 10
for attempt in range(max_retries):
async with await client.scoped_connection(routing_key="counter") as scope:
await scope.watch("counter")
val = await scope.get("counter")
new_val = str(int(val or "0") + 1)
await scope.multi()
await scope.set("counter", new_val)
result = await scope.exec()
if result is not None:
break # Success
# WATCH conflict — retry

Blocking commands honor their own blocking timeout rather than the client’s request timeout, so BLPOP key 0 blocks until an element arrives instead of failing when the request timeout elapses.

If a blocking command does time out or its caller cancels it, the scope’s connection is discarded on release instead of being returned to the pool. A server-side waiter may still be armed on it, and a later borrower could otherwise receive an element it never asked for.

Each client keeps its own pool of scope connections, holding up to 64 concurrent scopes. Acquiring beyond that limit waits for one to be released rather than failing immediately, so release each scope as soon as its transaction completes — a context manager or try/finally is the safest way to guarantee that. The wait ends at the acquire timeout (default 5 s) with a timeout error; causes that waiting cannot fix, such as a closed parent client or an invalid configuration, fail immediately instead of waiting.

When the parent client is configured for IAM authentication, a scope authenticates as the same IAM identity, and re-authenticates on its next command after the token rotates.

Re-authentication is deferred while it cannot be sent — inside an open MULTI (where it would be queued and corrupt the EXEC reply) — and resumes afterwards. If re-authentication fails, the connection is discarded rather than reused, so a scope never continues on a connection that can no longer authenticate.

When using isolated scopes with a cluster client, keep the following in mind:

Pass a routing_key when acquiring the scope. The key is used to determine which cluster node (via CRC16 slot hashing) the scope’s dedicated connection targets. If you omit it, the scope connects to the primary that owns slot 0 and is pinned to the slot of its first keyed command — which is not the same as passing a key that hashes to slot 0, since that constrains the scope to slot 0 from the start.

async with await client.scoped_connection(routing_key="user:123") as scope:
await scope.watch("user:123")
# ...

All keys used within a scope must hash to the same slot. If you attempt a cross-slot operation, the command will be rejected. Use hash tags (e.g., {user:123}:name and {user:123}:email) to ensure related keys land in the same slot.

A scope’s connection is pinned to the node that owned its slot when the scope was acquired. If the slot moves afterwards (for example during a rebalance), commands return a MOVED error rather than being redirected: a scope carries per-connection state such as WATCH keys and queued MULTI commands, and that state cannot follow a redirect to another node.

Treat it like any other scope failure — release the scope and acquire a new one, which connects to the slot’s current owner. The retry loop shown above handles this without special-casing.

The following apply to the preview release.

These are intentional constraints for this release. Design your application around them.

  • No Pub/Sub: Scoped connections do not support SUBSCRIBE, PSUBSCRIBE, or SSUBSCRIBE. Entering subscribed mode requires a dedicated message handler that a scope does not set up, so messages could never be delivered. Use the main client for Pub/Sub.
  • No MOVED/ASK redirects: By design, as described above — the error surfaces to the caller and the scope must be replaced.
  • Single slot per scope in cluster mode: A scope is pinned to the slot of its routing key, or, when none was passed, to the slot of its first keyed command. Later commands that route to a different slot are rejected with CROSSSLOT, matching the server’s requirement that WATCH and its MULTI/EXEC target the same slot.
  • A scope does not outlive its client: Closing the client, or the pool the client was borrowed from, invalidates any scope still open on it. Further commands on that scope fail.
  • No automatic reconnection: If the connection drops mid-scope, the command fails and the scope becomes unusable. Reconnecting silently would return a connection without the WATCH keys, queued MULTI commands, or tracking registrations the caller is relying on, so an EXEC could commit where it should have aborted. Acquire a new scope instead.
  • Single caller per scope: Only one coroutine or thread should use a scope at a time. Commands on one scope are serialized, but a scope has a single WATCH/MULTI state, so two callers sharing it will see each other’s queued commands and conflicts. Use one scope per caller.

These are being tracked and are expected to be resolved in a future release.

  • Resources can leak on cancelled blocking commands: If an unbounded blocking command (for example BLPOP with a timeout of 0) is cancelled on a scope, the underlying connection may leak. Prefer a non-zero timeout for blocking commands during the preview.
  • Scope connections keep the password the client was created with: updateConnectionPassword does not reach them — newly acquired scopes still send the old password, and idle scope connections are not re-authenticated. IAM auth is unaffected (scopes use the parent’s live token). Until this is fixed, rotate the password only while the old one is still accepted, or recreate the client.
  • TLS is not yet enforced for IAM authentication: IAM-authenticated scoped connections are not required to use TLS. Configure TLS explicitly when using IAM auth.