Skip to content

Commit 964b02c

Browse files
author
Aditya Patil
committed
fix: Moved registry refresh endpoint to REST and refactored UI refresh
Signed-off-by: Aditya Patil <adpatil@redhat.com>
1 parent 9fe258e commit 964b02c

8 files changed

Lines changed: 389 additions & 256 deletions

File tree

docs/reference/alpha-web-ui.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,11 @@ const tabsRegistry = {
154154

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

157-
## Refreshing the project list
157+
## Refreshing the registry
158158

159-
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.
159+
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.
160160

161-
To see new projects faster:
161+
To see changes faster:
162162

163163
- **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.
164-
- **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.
164+
- **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.

sdk/python/feast/api/registry/rest/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
from typing import Any, Optional
33

4-
from fastapi import FastAPI
4+
from fastapi import FastAPI, Response, status
55

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

47+
if resolved_store:
48+
49+
@app.post("/registry/refresh")
50+
def refresh_registry():
51+
try:
52+
resolved_store.refresh_registry()
53+
return Response(status_code=status.HTTP_200_OK)
54+
except Exception:
55+
logger.exception("Registry refresh failed")
56+
return Response(
57+
content='{"detail":"Registry refresh failed. Check server logs for details."}',
58+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
59+
media_type="application/json",
60+
)
61+
4762
_register_openlineage_consumer(app, resolved_store)
4863

4964

sdk/python/feast/ui_server.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,6 @@ async def feast_object_not_found_handler(
158158

159159
register_all_routes(rest_app, grpc_handler, store=store)
160160

161-
@rest_app.post("/registry/refresh")
162-
def refresh_registry():
163-
store.refresh_registry()
164-
return Response(status_code=status.HTTP_200_OK)
165-
166161
class PushRequest(BaseModel):
167162
push_source_name: str
168163
df: Dict[str, List]

sdk/python/tests/unit/test_ui_server.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,3 +296,21 @@ def test_registry_refresh_endpoint(mock_feature_store):
296296
resp = client.post("/api/v1/registry/refresh")
297297
assertpy.assert_that(resp.status_code).is_equal_to(EXPECTED_SUCCESS_STATUS)
298298
mock_feature_store.refresh_registry.assert_called_once()
299+
300+
301+
def test_registry_refresh_endpoint_error(mock_feature_store):
302+
"""POST /api/v1/registry/refresh returns 500 when refresh_registry raises."""
303+
mock_feature_store.refresh_registry = MagicMock(
304+
side_effect=Exception("registry unreachable")
305+
)
306+
307+
with tempfile.TemporaryDirectory() as temp_dir:
308+
_create_mock_ui_files(temp_dir)
309+
310+
with _setup_importlib_mocks(temp_dir):
311+
app = get_app(mock_feature_store, TEST_PROJECT_NAME)
312+
313+
client = TestClient(app)
314+
resp = client.post("/api/v1/registry/refresh")
315+
assertpy.assert_that(resp.status_code).is_equal_to(500)
316+
assertpy.assert_that(resp.json()["detail"]).contains("Registry refresh failed")
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React, { useContext } from "react";
2+
3+
interface RegistryRefreshContextInterface {
4+
refreshing: boolean;
5+
handleRefresh: () => Promise<void>;
6+
}
7+
8+
const RegistryRefreshContext = React.createContext<
9+
RegistryRefreshContextInterface | undefined
10+
>(undefined);
11+
12+
const useRegistryRefreshContext = () => {
13+
const ctx = useContext(RegistryRefreshContext);
14+
if (!ctx) {
15+
throw new Error(
16+
"useRegistryRefreshContext must be used within RegistryRefreshContext.Provider",
17+
);
18+
}
19+
return ctx;
20+
};
21+
22+
export { RegistryRefreshContext, useRegistryRefreshContext };

ui/src/hooks/useRegistryRefresh.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { useCallback, useContext, useState } from "react";
2+
import { useQueryClient } from "react-query";
3+
import {
4+
ProjectListContext,
5+
ProjectsListSchema,
6+
} from "../contexts/ProjectListContext";
7+
8+
interface Toast {
9+
id: string;
10+
title: string;
11+
color: "success" | "danger";
12+
iconType: string;
13+
}
14+
15+
const useRegistryRefresh = () => {
16+
const [refreshing, setRefreshing] = useState(false);
17+
const [toasts, setToasts] = useState<Toast[]>([]);
18+
const queryClient = useQueryClient();
19+
const projectListCtx = useContext(ProjectListContext);
20+
const basename = projectListCtx?.basename || "";
21+
22+
const removeToast = useCallback((removedToast: { id: string }) => {
23+
setToasts((prev) => prev.filter((t) => t.id !== removedToast.id));
24+
}, []);
25+
26+
const handleRefresh = useCallback(async () => {
27+
setRefreshing(true);
28+
try {
29+
const refreshRes = await fetch(`${basename}/api/v1/registry/refresh`, {
30+
method: "POST",
31+
});
32+
if (!refreshRes.ok) {
33+
throw new Error(`Registry refresh failed (${refreshRes.status})`);
34+
}
35+
const res = await fetch(`${basename}/projects-list.json`, {
36+
headers: { "Content-Type": "application/json" },
37+
});
38+
if (!res.ok) {
39+
throw new Error(`Failed to fetch project list (${res.status})`);
40+
}
41+
const json = await res.json();
42+
const parsed = ProjectsListSchema.parse(json);
43+
queryClient.setQueryData("feast-projects-list", parsed);
44+
await queryClient.invalidateQueries("registry-rest-bulk");
45+
setToasts((prev) => [
46+
...prev,
47+
{
48+
id: String(Date.now()),
49+
title: "Refresh successful",
50+
color: "success" as const,
51+
iconType: "check",
52+
},
53+
]);
54+
} catch {
55+
setToasts((prev) => [
56+
...prev,
57+
{
58+
id: String(Date.now()),
59+
title: "Refresh failed",
60+
color: "danger" as const,
61+
iconType: "alert",
62+
},
63+
]);
64+
} finally {
65+
setRefreshing(false);
66+
}
67+
}, [basename, queryClient]);
68+
69+
return { refreshing, toasts, handleRefresh, removeToast };
70+
};
71+
72+
export default useRegistryRefresh;

0 commit comments

Comments
 (0)