* vector: throttle worker discovery from lookup end
cli() set nextResolve before resolve() ran, so a lookup that takes as
long as the throttle interval (an unreachable first master costs the
full 5s dial timeout) left the next lookup already due when it
returned. Discovery then ran back to back and, since it holds d.mu,
every vector request queued behind it.
resolve() now sets nextResolve when the lookup lands, using the short
recheck window only while no client is cached.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vector: resolve workers outside the discovery lock
resolve() held d.mu across the master lookup, so while it waited out an
unreachable master every vector request blocked on cli(), even with a
healthy worker already cached.
The lookup now runs in a goroutine without d.mu, and d.resolving keeps
it to one at a time. Callers with a cached client use it while the
lookup is in flight; callers without one wait for that lookup and share
its result. Throttling, the sticky worker and reset(c) are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vector: keep the cached worker connection when a redial fails
resolve() closed d.conn before dialing the replacement worker. A
synchronous dial error then left d.client bound to a closed connection.
Requests got codes.Canceled, which never triggers reset, and later
refreshes saw the unchanged d.worker and never redialed. The old
connection is now closed only after the new dial succeeds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The vectors gateway backs off discovery for 5s between resolutions, but
the backoff also suppressed rediscovery after an empty lookup. A worker
that landed its KeepConnected registration a moment after the first
query stayed invisible for the whole window — longer than a client's
retry budget — so the S3 Vectors e2e flaked: CreateIndex raced the
registration, got InternalFailure, and every boto retry fell inside the
same 5s fast-fail window.
With no cached backend, recheck every 500ms instead; the 5s interval now
only bounds membership refreshes against a live client, and an absent
worker still cannot flood the master with lookups. The empty-membership
error also becomes codes.Unavailable so the API answers 503 rather than
InternalFailure.
cluster.ps lists registered vector nodes like the other node types, and
the e2e harness polls it for the engine's registration instead of only
checking the gRPC port — a listening engine is not discoverable until
the registration lands.