Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: Moved registry refresh endpoint to REST and refactored UI refresh
Signed-off-by: Aditya Patil <adpatil@redhat.com>
  • Loading branch information
Aditya Patil
Aditya Patil committed Jul 31, 2026
commit 0fe4725fb2b620b31843bb880d5fd4e368d4bd07
8 changes: 4 additions & 4 deletions docs/reference/alpha-web-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,11 @@ const tabsRegistry = {

Examples of custom tabs can be found in the `ui/custom-tabs` folder.

## Refreshing the project list
## Refreshing the registry

The Feast UI caches the project list using the same registry cache. After running `feast apply` to add a new project, it may take up to `cache_ttl_seconds` before the project appears in the UI.
The Feast UI caches registry data (projects, feature views, entities, etc.) using the registry cache. After running `feast apply` to make changes, it may take up to `cache_ttl_seconds` before the updates appear in the UI.

To see new projects faster:
To see changes faster:

- **Lower the TTL**: Set `cache_ttl_seconds: 10` (or similar) in your `feature_store.yaml` registry config. This makes all registry consumers — including the UI — pick up changes within 10 seconds.
- **Refresh on demand**: The project selection page has a **Refresh** button that explicitly invalidates the server-side registry cache (`POST /api/v1/registry/refresh`) and reloads the project list without a full page refresh.
- **Refresh on demand**: The UI has a **Refresh** button that explicitly invalidates the server-side registry cache (`POST /api/v1/registry/refresh`) and reloads the UI without a full page refresh.
17 changes: 16 additions & 1 deletion sdk/python/feast/api/registry/rest/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
from typing import Any, Optional

from fastapi import FastAPI
from fastapi import FastAPI, Response, status

from feast.api.registry.rest.compute_engines import get_compute_engine_router
from feast.api.registry.rest.data_sources import get_data_source_router
Expand Down Expand Up @@ -44,6 +44,21 @@ def register_all_routes(app: FastAPI, grpc_handler, server=None, store=None):
app.include_router(get_monitoring_router(grpc_handler, store=resolved_store))
app.include_router(get_compute_engine_router(grpc_handler, store=resolved_store))

if resolved_store:

@app.post("/registry/refresh")
Comment thread
ntkathole marked this conversation as resolved.
Outdated
def refresh_registry():
try:
resolved_store.refresh_registry()
Comment thread
ntkathole marked this conversation as resolved.
Outdated
return Response(status_code=status.HTTP_200_OK)
except Exception:
Comment thread
ntkathole marked this conversation as resolved.
Outdated
logger.exception("Registry refresh failed")
return Response(
content='{"detail":"Registry refresh failed. Check server logs for details."}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
media_type="application/json",
)

_register_openlineage_consumer(app, resolved_store)


Expand Down
5 changes: 0 additions & 5 deletions sdk/python/feast/ui_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,6 @@ async def feast_object_not_found_handler(

register_all_routes(rest_app, grpc_handler, store=store)

@rest_app.post("/registry/refresh")
def refresh_registry():
store.refresh_registry()
return Response(status_code=status.HTTP_200_OK)

class PushRequest(BaseModel):
push_source_name: str
df: Dict[str, List]
Expand Down
18 changes: 18 additions & 0 deletions sdk/python/tests/unit/test_ui_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,21 @@ def test_registry_refresh_endpoint(mock_feature_store):
resp = client.post("/api/v1/registry/refresh")
assertpy.assert_that(resp.status_code).is_equal_to(EXPECTED_SUCCESS_STATUS)
mock_feature_store.refresh_registry.assert_called_once()


def test_registry_refresh_endpoint_error(mock_feature_store):
"""POST /api/v1/registry/refresh returns 500 when refresh_registry raises."""
mock_feature_store.refresh_registry = MagicMock(
side_effect=Exception("registry unreachable")
)

with tempfile.TemporaryDirectory() as temp_dir:
_create_mock_ui_files(temp_dir)

with _setup_importlib_mocks(temp_dir):
app = get_app(mock_feature_store, TEST_PROJECT_NAME)

client = TestClient(app)
resp = client.post("/api/v1/registry/refresh")
assertpy.assert_that(resp.status_code).is_equal_to(500)
assertpy.assert_that(resp.json()["detail"]).contains("Registry refresh failed")
22 changes: 22 additions & 0 deletions ui/src/contexts/RegistryRefreshContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { useContext } from "react";

interface RegistryRefreshContextInterface {
refreshing: boolean;
handleRefresh: () => Promise<void>;
}

const RegistryRefreshContext = React.createContext<
RegistryRefreshContextInterface | undefined
>(undefined);

const useRegistryRefreshContext = () => {
const ctx = useContext(RegistryRefreshContext);
if (!ctx) {
throw new Error(
"useRegistryRefreshContext must be used within RegistryRefreshContext.Provider",
);
}
return ctx;
};

export { RegistryRefreshContext, useRegistryRefreshContext };
72 changes: 72 additions & 0 deletions ui/src/hooks/useRegistryRefresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useCallback, useContext, useState } from "react";
import { useQueryClient } from "react-query";
import {
ProjectListContext,
ProjectsListSchema,
} from "../contexts/ProjectListContext";

interface Toast {
id: string;
title: string;
color: "success" | "danger";
iconType: string;
}

const useRegistryRefresh = () => {
const [refreshing, setRefreshing] = useState(false);
const [toasts, setToasts] = useState<Toast[]>([]);
const queryClient = useQueryClient();
const projectListCtx = useContext(ProjectListContext);
const basename = projectListCtx?.basename || "";

const removeToast = useCallback((removedToast: { id: string }) => {
setToasts((prev) => prev.filter((t) => t.id !== removedToast.id));
}, []);

const handleRefresh = useCallback(async () => {
setRefreshing(true);
try {
const refreshRes = await fetch(`${basename}/api/v1/registry/refresh`, {
Comment thread
ntkathole marked this conversation as resolved.
method: "POST",
});
if (!refreshRes.ok) {
throw new Error(`Registry refresh failed (${refreshRes.status})`);
}
const res = await fetch(`${basename}/projects-list.json`, {
headers: { "Content-Type": "application/json" },
});
if (!res.ok) {
throw new Error(`Failed to fetch project list (${res.status})`);
}
const json = await res.json();
const parsed = ProjectsListSchema.parse(json);
queryClient.setQueryData("feast-projects-list", parsed);
await queryClient.invalidateQueries("registry-rest-bulk");
setToasts((prev) => [
...prev,
{
id: String(Date.now()),
title: "Refresh successful",
color: "success" as const,
iconType: "check",
},
]);
} catch {
setToasts((prev) => [
...prev,
{
id: String(Date.now()),
title: "Refresh failed",
color: "danger" as const,
iconType: "alert",
},
]);
} finally {
setRefreshing(false);
}
}, [basename, queryClient]);

return { refreshing, toasts, handleRefresh, removeToast };
};

export default useRegistryRefresh;
Loading