GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

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.

Sign up for GitHub

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

Merged
ntkathole merged 8 commits into feast-dev:master from dbbvitor:fix/sdk-dual-stack-binds
Oct 1, 2026

Uh oh!

There was an error while loading. Please reload this page.

Merged

fix: Bind metrics, REST registry, ui, and lineage servers dual-stack #6886

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

Clear filters
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
Diff view

There are no files selected for viewing

40 changes: 25 additions & 15 deletions 40 sdk/python/feast/api/registry/rest/rest_registry_server.py
Show comments
View file
Open in desktop
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

@ntkathole ntkathole Sep 29, 2026

Copy link
Copy Markdown
Member

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.

All reactions

@dbbvitor dbbvitor Sep 29, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

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.

All reactions

@dbbvitor dbbvitor Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

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.

Here is an example of the override pattern on the follow up PR: https://github.com/feast-dev/feast/pull/6887/changes#diff-9a160662c77106b5759918b6ee5293ab38814aca9c0eb035199c5b4397d1cfe4R3644

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
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)
19 changes: 17 additions & 2 deletions 19 sdk/python/feast/cli/serve.py
Show comments
View file
Open in desktop
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
9 changes: 7 additions & 2 deletions 9 sdk/python/feast/feature_store.py
Show comments
View file
Open in desktop
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
13 changes: 12 additions & 1 deletion 13 sdk/python/feast/lineage_server.py
Show comments
View file
Open in desktop
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)
28 changes: 24 additions & 4 deletions 28 sdk/python/feast/metrics.py
Show comments
View file
Open in desktop
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
22 changes: 14 additions & 8 deletions 22 sdk/python/feast/ui_server.py
Show comments
View file
Open in desktop
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)
46 changes: 46 additions & 0 deletions 46 sdk/python/feast/utils.py
Show comments
View file
Open in desktop
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

@ntkathole ntkathole Sep 29, 2026

Copy link
Copy Markdown
Member

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.

All reactions

@dbbvitor dbbvitor Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

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.

All reactions
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.

Comment thread
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.

Comment thread
sock.listen(socket.SOMAXCONN)
sock.setblocking(blocking)
except Exception:
sock.close()
raise
return sock
Comment thread

@ntkathole ntkathole Sep 29, 2026

Copy link
Copy Markdown
Member

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.

All reactions

@dbbvitor dbbvitor Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

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.

All reactions


Comment thread

@ntkathole ntkathole Sep 29, 2026

Copy link
Copy Markdown
Member

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.

All reactions

@dbbvitor dbbvitor Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

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.

All reactions
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.
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.