Expected Behavior
A registry RPC issued by RemoteRegistry should fail within a bounded time when
the connection to the registry server stops making progress, and the bound
should be configurable through RemoteRegistryConfig.
Current Behavior
RemoteRegistry builds a channel with no keepalive and issues every RPC with no
deadline, so a connection that stops delivering data but stays ESTABLISHED
leaves the caller parked in epoll_wait forever.
Three things combine:
RemoteRegistryConfig (sdk/python/feast/infra/registry/remote.py) exposes
path, cert, is_tls, client_cert, client_key and authority. There
is no timeout field and no keepalive field.
RemoteRegistry._create_grpc_channel populates options only with
grpc.default_authority. No grpc.keepalive_time_ms or related option is set.
- None of the RPC call sites pass a deadline.
remote.py has 50
self.stub.<Rpc>(...) calls on v0.64.0 and 51 on v0.65.0, with zero
occurrences of timeout= in either.
There is also no supported way to substitute an implementation.
get_registry_config_from_type validates registry_type against the hardcoded
REGISTRY_CLASS_FOR_TYPE map with the comment # We do not support custom registry's right now, and FeatureStore._create_registry instantiates
RemoteRegistry directly, so a subclass is never reachable. This is unlike the
online store, offline store and batch engine, which all accept a dotted class
path.
One related footgun: RegistryConfig extends FeastBaseModel, whose
model_config is extra="allow". An invented key such as grpc_options in the
registry block therefore validates cleanly and is silently ignored, because
nothing reads it. A user attempting to configure this gets no error.
Observed impact. In a long-running batch job that writes to the offline
store, a registry fetch on a blackholed network path never returns. The writer
thread never completes, the write queue fills, every producer blocks on it, and
the pod sits at roughly 1m CPU with 0 restarts until the node is reclaimed.
Nothing in the logs indicates a stuck RPC, because no error is ever raised.
Steps to reproduce
-
Configure a remote registry:
project: demo
registry:
registry_type: remote
path: registry.example:80
-
Make the registry endpoint blackhole traffic after the TCP connection is
established — for example drop packets to that address with a firewall rule
(iptables -A OUTPUT -d <registry ip> -p tcp -j DROP) after the first
successful call, or route through a proxy that accepts the connection and then
stops responding.
-
Call any registry method, e.g. store.get_feature_view("some_fv").
-
The call does not return. ss -tnp shows the socket still ESTABLISHED;
py-spy dump shows the thread in epoll_wait under
grpc._channel._UnaryUnaryMultiCallable.__call__. There is no timeout and no
exception.
Adding a timeout through configuration is not possible: registry.grpc_options
or registry.timeout are accepted by pydantic and ignored, and
registry_type: my.module.MyRegistry is rejected by
FeastRegistryTypeInvalidError.
Specifications
- Version: 0.64.0; also verified present on 0.65.0 (latest release) and
master
- Platform: Linux, Python 3.12, Kubernetes; remote registry over gRPC
- Subsystem: registry —
feast.infra.registry.remote.RemoteRegistry
Possible Solution
Two parts, both small:
-
Add optional timeout and keepalive fields to RemoteRegistryConfig, and
apply them in _create_grpc_channel. A default deadline is the load-bearing
half: the hang always has an RPC in flight, so a deadline bounds it, requires
no server agreement, and cannot be refused by an intermediary. Keepalive is
the wider net, since it also catches a channel that rots while idle.
-
Rather than touching 50 call sites, install a
grpc.UnaryUnaryClientInterceptor that sets a default timeout on any call
issued without one. Because Feast already layers
GrpcClientAuthHeaderInterceptor over the channel, the two compose and every
RPC gains a deadline with no call-site changes.
On grpc.keepalive_permit_without_calls: it should be left unset, or at least
not defaulted on. gRPC servers enforce
GRPC_ARG_HTTP2_MIN_RECV_PING_INTERVAL_WITHOUT_DATA_MS (5 minutes by default)
and answer too-frequent pings with a GOAWAY carrying too_many_pings, which
would kill working connections.
A secondary fix worth considering independently: let registry_type accept a
dotted class path, as the online and offline stores already do. That would give
users an escape hatch for this class of problem without a patch.
We currently monkeypatch RemoteRegistry._create_grpc_channel to add keepalive
options and wrap the channel in a deadline interceptor. Happy to open a PR along
the lines of (1) and (2) if the approach looks right.
Expected Behavior
A registry RPC issued by
RemoteRegistryshould fail within a bounded time whenthe connection to the registry server stops making progress, and the bound
should be configurable through
RemoteRegistryConfig.Current Behavior
RemoteRegistrybuilds a channel with no keepalive and issues every RPC with nodeadline, so a connection that stops delivering data but stays
ESTABLISHEDleaves the caller parked in
epoll_waitforever.Three things combine:
RemoteRegistryConfig(sdk/python/feast/infra/registry/remote.py) exposespath,cert,is_tls,client_cert,client_keyandauthority. Thereis no timeout field and no keepalive field.
RemoteRegistry._create_grpc_channelpopulatesoptionsonly withgrpc.default_authority. Nogrpc.keepalive_time_msor related option is set.remote.pyhas 50self.stub.<Rpc>(...)calls onv0.64.0and 51 onv0.65.0, with zerooccurrences of
timeout=in either.There is also no supported way to substitute an implementation.
get_registry_config_from_typevalidatesregistry_typeagainst the hardcodedREGISTRY_CLASS_FOR_TYPEmap with the comment# We do not support custom registry's right now, andFeatureStore._create_registryinstantiatesRemoteRegistrydirectly, so a subclass is never reachable. This is unlike theonline store, offline store and batch engine, which all accept a dotted class
path.
One related footgun:
RegistryConfigextendsFeastBaseModel, whosemodel_configisextra="allow". An invented key such asgrpc_optionsin theregistryblock therefore validates cleanly and is silently ignored, becausenothing reads it. A user attempting to configure this gets no error.
Observed impact. In a long-running batch job that writes to the offline
store, a registry fetch on a blackholed network path never returns. The writer
thread never completes, the write queue fills, every producer blocks on it, and
the pod sits at roughly 1m CPU with 0 restarts until the node is reclaimed.
Nothing in the logs indicates a stuck RPC, because no error is ever raised.
Steps to reproduce
Configure a remote registry:
Make the registry endpoint blackhole traffic after the TCP connection is
established — for example drop packets to that address with a firewall rule
(
iptables -A OUTPUT -d <registry ip> -p tcp -j DROP) after the firstsuccessful call, or route through a proxy that accepts the connection and then
stops responding.
Call any registry method, e.g.
store.get_feature_view("some_fv").The call does not return.
ss -tnpshows the socket stillESTABLISHED;py-spy dumpshows the thread inepoll_waitundergrpc._channel._UnaryUnaryMultiCallable.__call__. There is no timeout and noexception.
Adding a timeout through configuration is not possible:
registry.grpc_optionsor
registry.timeoutare accepted by pydantic and ignored, andregistry_type: my.module.MyRegistryis rejected byFeastRegistryTypeInvalidError.Specifications
masterfeast.infra.registry.remote.RemoteRegistryPossible Solution
Two parts, both small:
Add optional
timeoutand keepalive fields toRemoteRegistryConfig, andapply them in
_create_grpc_channel. A default deadline is the load-bearinghalf: the hang always has an RPC in flight, so a deadline bounds it, requires
no server agreement, and cannot be refused by an intermediary. Keepalive is
the wider net, since it also catches a channel that rots while idle.
Rather than touching 50 call sites, install a
grpc.UnaryUnaryClientInterceptorthat sets a defaulttimeouton any callissued without one. Because Feast already layers
GrpcClientAuthHeaderInterceptorover the channel, the two compose and everyRPC gains a deadline with no call-site changes.
On
grpc.keepalive_permit_without_calls: it should be left unset, or at leastnot defaulted on. gRPC servers enforce
GRPC_ARG_HTTP2_MIN_RECV_PING_INTERVAL_WITHOUT_DATA_MS(5 minutes by default)and answer too-frequent pings with a
GOAWAYcarryingtoo_many_pings, whichwould kill working connections.
A secondary fix worth considering independently: let
registry_typeaccept adotted class path, as the online and offline stores already do. That would give
users an escape hatch for this class of problem without a patch.
We currently monkeypatch
RemoteRegistry._create_grpc_channelto add keepaliveoptions and wrap the channel in a deadline interceptor. Happy to open a PR along
the lines of (1) and (2) if the approach looks right.