GeezerVison is showing github.com — fix: Bind metrics, REST registry, ui, and lineage servers dual-stack by dbbvitor · Pull Request #6886 · feast-dev/feast
fix: Bind metrics, REST registry, ui, and lineage servers dual-stack
#6886
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and
privacy statement . We’ll occasionally send you account related emails.
Already on GitHub?
Sign in
to your account
Uh oh!
There was an error while loading. Please reload this page.
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
140d837
fix: Bind metrics and REST registry servers dual-stack
dbbvitor Sep 24, 2026
a7fe15b
style: Add type hints to _make_metrics_httpd, restore stray formatting
dbbvitor Sep 24, 2026
63c5b6d
fix: Bind the REST registry server via a pre-built dual-stack socket
dbbvitor Sep 24, 2026
2531291
fix: Bind the ui and lineage servers dual-stack too
dbbvitor Sep 25, 2026
e769fd0
refactor: Share one dual-stack socket builder across REST, ui, lineage
dbbvitor Sep 25, 2026
3c93662
fix: Close the dual-stack socket on bind failure, cache IPv6 probe
dbbvitor Sep 30, 2026
2ce805a
feat: Expose a host override on the REST registry server
dbbvitor Sep 30, 2026
202fde8
fix: Add a blocking parameter to _make_dual_stack_socket
dbbvitor Sep 30, 2026
File filter
Filter by extension
.py
(14)
All 1 file type selected
Viewed files
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Hide whitespace
Diff view
Hide whitespace
There are no files selected for viewing
Original file line number
Diff line number
Diff line change
Expand Up
@@ -23,7 +23,7 @@
str_to_auth_manager_type,
)
from feast.registry_server import RegistryServer
from feast.utils import _utc_now
from feast.utils import _make_dual_stack_socket, _utc_now
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
Expand Down
Expand Up
@@ -322,30 +322,40 @@ def _init_auth(self):
def start_server(
self,
port: int,
host: str = "::",
tls_key_path: str = "",
tls_cert_path: str = "",
):
"""Start the REST registry server. host="::" (default) binds dual-stack
via a pre-built socket; any other host falls back to plain uvicorn.run."""
import uvicorn
from feast.registry_server import _sync_protected_project_tag
_sync_protected_project_tag(self.store)
Comment thread
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more .
Unlike ui_server and lineage_server (which gate on host == "::"), this unconditionally switches from the old host="0.0.0.0" to dual-stack. On dual-stack hosts this changes the listen address from 0.0.0.0 to [::], which could conflict with firewall rules or existing port binds. Is the unconditional switch intentional, or should this also be opt-in to match the other two servers?
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
dbbvitor
Sep 29, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more .
It was intentional, but it can default to '0.0.0.0' as before, as the follow-up PR can expose an override on the CRD.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more .
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
ssl_kwargs: dict = {}
if tls_key_path and tls_cert_path:
logger.info("Starting REST registry server in TLS(SSL) mode")
logger.info(f"REST registry server listening on https://localhost:{port}")
uvicorn.run(
self.app,
host="0.0.0.0",
port=port,
ssl_keyfile=tls_key_path,
ssl_certfile=tls_cert_path,
ssl_kwargs = {"ssl_keyfile": tls_key_path, "ssl_certfile": tls_cert_path}
scheme = "https"
else:
scheme = "http"
if host == "::":
logger.info(
f"Starting REST registry server in "
f"{'TLS(SSL)' if ssl_kwargs else 'non-TLS(SSL)'} mode"
)
logger.info(
f"REST registry server listening on {scheme}://localhost:{port}"
)
sock = _make_dual_stack_socket(port)
config = uvicorn.Config(self.app, **ssl_kwargs)
uvicorn.Server(config).run(sockets=[sock])
else:
logger.info("Starting REST registry server in non-TLS(SSL) mode")
logger.info(f"REST registry server listening on http://localhost:{port}")
uvicorn.run(
self.app,
host="0.0.0.0",
port=port,
logger.info(
f"Starting REST registry server in "
f"{'TLS(SSL)' if ssl_kwargs else 'non-TLS(SSL)'} mode"
)
logger.info(f"REST registry server listening on {scheme}://{host}:{port}")
uvicorn.run(self.app, host=host, port=port, **ssl_kwargs)
Original file line number
Diff line number
Diff line change
Expand Up
@@ -225,6 +225,14 @@ def serve_transformations_command(ctx: click.Context, port: int):
show_default=True,
help="Start a REST API Registry Server",
)
@click.option(
"--host",
"-h",
type=click.STRING,
default="::",
show_default=True,
help="Bind address for the REST API Registry Server (ignored for gRPC).",
)
@click.pass_context
def serve_registry_command(
ctx: click.Context,
Expand All
@@ -234,6 +242,7 @@ def serve_registry_command(
grpc: bool,
rest_api: bool,
rest_port: int,
host: str,
):
"""Start Feast Registry server (gRPC by default, REST opt-in)."""
if (tls_key_path and not tls_cert_path) or (not tls_key_path and tls_cert_path):
Expand All
@@ -253,7 +262,7 @@ def serve_registry_command(
),
multiprocessing.Process(
target=_serve_rest_registry,
args=(repo_path, rest_port, tls_key_path, tls_cert_path),
args=(repo_path, rest_port, tls_key_path, tls_cert_path, host ),
name="rest_registry_server",
),
]
Expand All
@@ -272,6 +281,7 @@ def serve_registry_command(
tls_key_path=tls_key_path,
tls_cert_path=tls_cert_path,
rest_api=rest_api,
host=host,
)
Expand All
@@ -289,7 +299,11 @@ def _serve_grpc_registry(
def _serve_rest_registry(
repo_path: str, port: int, tls_key_path: str, tls_cert_path: str
repo_path: str,
port: int,
tls_key_path: str,
tls_cert_path: str,
host: str,
):
from feast import FeatureStore
Expand All
@@ -299,6 +313,7 @@ def _serve_rest_registry(
tls_key_path=tls_key_path,
tls_cert_path=tls_cert_path,
rest_api=True,
host=host,
)
Expand Down
Original file line number
Diff line number
Diff line change
Expand Up
@@ -4765,15 +4765,20 @@ def serve_registry(
tls_key_path: str = "",
tls_cert_path: str = "",
rest_api: bool = False,
host: str = "::",
) -> None:
"""Start registry server locally on a given port."""
"""Start registry server locally on a given port. `host` only applies
to the REST server (rest_api=True); defaults to dual-stack "::"."""
if rest_api:
from feast.api.registry.rest import rest_registry_server
server = rest_registry_server.RestRegistryServer(self)
server.start_server(
port=port, tls_key_path=tls_key_path, tls_cert_path=tls_cert_path
port=port,
host=host,
tls_key_path=tls_key_path,
tls_cert_path=tls_cert_path,
)
else:
from feast import registry_server
Expand Down
Original file line number
Diff line number
Diff line change
Expand Up
@@ -256,4 +256,15 @@ def start_lineage_server(
import uvicorn
logger.info(f"Starting Feast OpenLineage server on {scheme}://{host}:{port}")
uvicorn.run(app, host=host, port=port, **ssl_kwargs)
if host == "::":
# Plain uvicorn.run(host="::") binds IPv6-only (see
# feast.utils._make_dual_stack_socket), which would drop IPv4
# clients on every host, not just IPv6-less ones.
from feast.utils import _make_dual_stack_socket
sock = _make_dual_stack_socket(port)
config = uvicorn.Config(app, **ssl_kwargs)
uvicorn.Server(config).run(sockets=[sock])
else:
uvicorn.run(app, host=host, port=port, **ssl_kwargs)
Original file line number
Diff line number
Diff line change
Expand Up
@@ -46,13 +46,15 @@
import logging
import os
import shutil
import socket
import tempfile
import threading
import time
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import TYPE_CHECKING, List, Optional
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional
from wsgiref.simple_server import WSGIServer, make_server
import psutil
Expand Down
Expand Up
@@ -583,6 +585,26 @@ def init_worker_freshness_monitoring(store: "FeatureStore"):
t.start()
def _make_metrics_httpd(port: int, app: Callable[..., Iterable[bytes]]) -> WSGIServer:
"""Build the metrics HTTP server, dual-stack ("::") when the host
supports IPv6, or IPv4-only (falls back to `make_server`'s default
behavior) otherwise.
"""
from feast import utils
if not utils._ipv6_available():
return make_server("", port, app)
class DualStackWSGIServer(WSGIServer):
address_family = socket.AF_INET6
def server_bind(self) -> None:
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
super().server_bind()
return make_server("::", port, app, server_class=DualStackWSGIServer)
def start_metrics_server(
store: "FeatureStore",
port: int = 8000,
Expand Down
Expand Up
@@ -633,9 +655,7 @@ def start_metrics_server(
registry = CollectorRegistry()
MultiProcessCollector(registry)
from wsgiref.simple_server import make_server
httpd = make_server("", port, make_wsgi_app(registry))
httpd = _make_metrics_httpd(port, make_wsgi_app(registry))
metrics_thread = threading.Thread(target=httpd.serve_forever, daemon=True)
metrics_thread.start()
logger.info(
Expand Down
Original file line number
Diff line number
Diff line change
Expand Up
@@ -1216,13 +1216,19 @@ def start_server(
logger.info(f"Starting Feast UI server on {host}:{port}")
ssl_kwargs: dict = {}
if tls_key_path and tls_cert_path:
uvicorn.run(
app,
host=host,
port=port,
ssl_keyfile=tls_key_path,
ssl_certfile=tls_cert_path,
)
ssl_kwargs["ssl_keyfile"] = tls_key_path
ssl_kwargs["ssl_certfile"] = tls_cert_path
if host == "::":
# Plain uvicorn.run(host="::") binds IPv6-only (see
# feast.utils._make_dual_stack_socket), which would drop IPv4
# clients on every host, not just IPv6-less ones.
from feast.utils import _make_dual_stack_socket
sock = _make_dual_stack_socket(port)
config = uvicorn.Config(app, **ssl_kwargs)
uvicorn.Server(config).run(sockets=[sock])
else:
uvicorn.run(app, host=host, port=port)
uvicorn.run(app, host=host, port=port, **ssl_kwargs )
Original file line number
Diff line number
Diff line change
@@ -1,7 +1,9 @@
import copy
import functools
import itertools
import logging
import os
import socket
import threading
import typing
import warnings
Expand Down
Expand Up
@@ -63,6 +65,50 @@
USER_AGENT = "{}/{}".format(APPLICATION_NAME, get_version())
@functools.lru_cache()
def _ipv6_available() -> bool:
"""Whether this process can bind AF_INET6 (cached: can't change at runtime)."""
try:
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock:
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
Comment thread
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more .
Nit: IPv6 support can't change during a process lifetime — caching with @functools.lru_cache() would make that intent explicit and skip the throwaway socket on repeated calls.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more .
Added @functools.lru_cache() decorator to the function.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
sock.bind(("::", 0))
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Uh oh!
There was an error while loading. Please reload this page.
return True
except OSError:
return False
def _make_dual_stack_socket(port: int, blocking: bool = False) -> socket.socket:
"""Pre-bind a dual-stack socket for a server to adopt.
A framework handed host="::" directly (uvicorn.run, loop.create_server)
sets IPV6_V6ONLY=1 on its own socket, silently dropping IPv4 clients.
Binding here with V6ONLY cleared avoids that; falls back to IPv4-only
when the host can't bind AF_INET6.
Non-blocking by default since every current caller hands the socket to
an asyncio-based server (uvicorn.Server(...).run(sockets=[sock])), which
requires that; pass blocking=True for a synchronous server instead.
"""
dual_stack = _ipv6_available()
if dual_stack:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
address: tuple = ("::", port)
else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
address = ("0.0.0.0", port)
try:
if dual_stack:
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(address)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Uh oh!
There was an error while loading. Please reload this page.
sock.listen(socket.SOMAXCONN)
sock.setblocking(blocking)
except Exception:
sock.close()
raise
return sock
Comment thread
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more .
Socket leak: if bind() or listen() throws (e.g. port already in use), the socket is never closed. Wrap the post-creation steps:
try :
sock .setsockopt (socket .SOL_SOCKET , socket .SO_REUSEADDR , 1 )
sock .bind (address )
sock .listen (socket .SOMAXCONN )
sock .setblocking (False )
except :
sock .close ()
raise
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more .
Try-except pattern adopted
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Comment thread
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more .
setblocking(False) bakes in an asyncio assumption. The function name is generic, but a caller expecting a blocking socket would fail silently. Consider either documenting this in the docstring or adding a blocking=False parameter.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more .
Added the blocking parameter to the func, defaulting to False, and added it to the docstring.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
def _parse_feature_or_view_ref(ref: str) -> Tuple[str, Optional[int], Optional[str]]:
"""Parse 'fv_name[@version][:feature]' into (fv_name, version_number, feature_name).
Expand Down
Loading
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Loading
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.