mirror of
https://github.com/hansjone/netx.git
synced 2026-10-11 15:30:45 +08:00
feat(ume): handle server-side subscription loss with local cleanup confirm
Detect missing UME subscriptions from WSS/DELETE errors, stop stale reconnects, and prompt to clear local state before re-establishing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
31e745ef95
commit
fb374a9c36
6 changed files with 353 additions and 39 deletions
|
|
@ -36,6 +36,7 @@ from .parser_config import load_parser_config
|
||||||
from .ume_client import UMEClient
|
from .ume_client import UMEClient
|
||||||
from .ume_alarm_ws import (
|
from .ume_alarm_ws import (
|
||||||
cancel_alarm_subscription_manual,
|
cancel_alarm_subscription_manual,
|
||||||
|
clear_local_alarm_subscription_manual,
|
||||||
establish_alarm_subscription_manual,
|
establish_alarm_subscription_manual,
|
||||||
get_subscription_status,
|
get_subscription_status,
|
||||||
get_ws_connection_status,
|
get_ws_connection_status,
|
||||||
|
|
@ -946,10 +947,15 @@ def ume_alarm_subscription_status(limit: int = 80) -> dict[str, Any]:
|
||||||
|
|
||||||
|
|
||||||
@app.post("/v1/ume/alarm-subscription/establish")
|
@app.post("/v1/ume/alarm-subscription/establish")
|
||||||
def ume_alarm_subscription_establish(db: Session = Depends(get_db)) -> dict[str, Any]:
|
def ume_alarm_subscription_establish(
|
||||||
|
payload: dict[str, Any] | None = None,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict[str, Any]:
|
||||||
client = _ume_client()
|
client = _ume_client()
|
||||||
|
body = payload or {}
|
||||||
|
force_reestablish = bool(body.get("force_reestablish"))
|
||||||
try:
|
try:
|
||||||
st = establish_alarm_subscription_manual(client, db)
|
st = establish_alarm_subscription_manual(client, db, force_reestablish=force_reestablish)
|
||||||
return {"ok": True, "created": not bool(st.get("already_exists")), **st}
|
return {"ok": True, "created": not bool(st.get("already_exists")), **st}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
msg = str(exc)[:240]
|
msg = str(exc)[:240]
|
||||||
|
|
@ -957,16 +963,33 @@ def ume_alarm_subscription_establish(db: Session = Depends(get_db)) -> dict[str,
|
||||||
|
|
||||||
|
|
||||||
@app.post("/v1/ume/alarm-subscription/cancel")
|
@app.post("/v1/ume/alarm-subscription/cancel")
|
||||||
def ume_alarm_subscription_cancel(db: Session = Depends(get_db)) -> dict[str, Any]:
|
def ume_alarm_subscription_cancel(
|
||||||
|
payload: dict[str, Any] | None = None,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict[str, Any]:
|
||||||
client = _ume_client()
|
client = _ume_client()
|
||||||
|
body = payload or {}
|
||||||
|
force_clear_local = bool(body.get("force_clear_local"))
|
||||||
try:
|
try:
|
||||||
st = cancel_alarm_subscription_manual(client, db)
|
st = cancel_alarm_subscription_manual(client, db, force_clear_local=force_clear_local)
|
||||||
|
if st.get("needs_local_cleanup"):
|
||||||
|
return st
|
||||||
return {"ok": True, **st}
|
return {"ok": True, **st}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
msg = str(exc)[:240]
|
msg = str(exc)[:240]
|
||||||
raise HTTPException(status_code=502, detail=msg) from exc
|
raise HTTPException(status_code=502, detail=msg) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/v1/ume/alarm-subscription/clear-local")
|
||||||
|
def ume_alarm_subscription_clear_local(db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
st = clear_local_alarm_subscription_manual(db)
|
||||||
|
return {"ok": True, "cleared_local": True, **st}
|
||||||
|
except Exception as exc:
|
||||||
|
msg = str(exc)[:240]
|
||||||
|
raise HTTPException(status_code=502, detail=msg) from exc
|
||||||
|
|
||||||
|
|
||||||
@app.post("/v1/ume/sync")
|
@app.post("/v1/ume/sync")
|
||||||
def ume_sync(payload: dict[str, Any] | None = None, db: Session = Depends(get_db)) -> dict[str, Any]:
|
def ume_sync(payload: dict[str, Any] | None = None, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||||
body = payload or {}
|
body = payload or {}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,22 @@ _WS_LOG_MAX_RETURN = 100
|
||||||
|
|
||||||
_ORPHAN_SUB_ID_RE = re.compile(r"id:([0-9a-fA-F-]{8}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{12})")
|
_ORPHAN_SUB_ID_RE = re.compile(r"id:([0-9a-fA-F-]{8}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{12})")
|
||||||
|
|
||||||
|
_SUBSCRIPTION_MISSING_MARKERS: tuple[str, ...] = (
|
||||||
|
"subscription not exist",
|
||||||
|
"subscription not found",
|
||||||
|
"not found or overtime",
|
||||||
|
"please establish again",
|
||||||
|
"status-code: 404",
|
||||||
|
"status-reason: subscription not found",
|
||||||
|
"non-101 status: 503",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_ume_subscription_missing_error(message: str) -> bool:
|
||||||
|
"""True when UME reports the notification subscription is gone or expired."""
|
||||||
|
low = str(message or "").lower()
|
||||||
|
return any(marker in low for marker in _SUBSCRIPTION_MISSING_MARKERS)
|
||||||
|
|
||||||
|
|
||||||
def _utc_now_iso() -> str:
|
def _utc_now_iso() -> str:
|
||||||
return datetime.now(timezone.utc).isoformat()
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
@ -98,6 +114,9 @@ _subscription_topic: str = "ALARM"
|
||||||
_active_client: UMEClient | None = None
|
_active_client: UMEClient | None = None
|
||||||
_ws_connection_state: str = "init"
|
_ws_connection_state: str = "init"
|
||||||
_ws_connection_detail: str = ""
|
_ws_connection_detail: str = ""
|
||||||
|
_ume_lost_lock = threading.Lock()
|
||||||
|
_ume_subscription_lost = False
|
||||||
|
_ume_subscription_lost_reason: str = ""
|
||||||
|
|
||||||
_WS_CONNECTION_LABELS: dict[str, str] = {
|
_WS_CONNECTION_LABELS: dict[str, str] = {
|
||||||
"init": "初始化",
|
"init": "初始化",
|
||||||
|
|
@ -109,9 +128,42 @@ _WS_CONNECTION_LABELS: dict[str, str] = {
|
||||||
"paused": "已暂停",
|
"paused": "已暂停",
|
||||||
"error": "连接异常",
|
"error": "连接异常",
|
||||||
"reconnecting": "重连等待",
|
"reconnecting": "重连等待",
|
||||||
|
"subscription_lost": "UME订阅已丢失",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mark_ume_subscription_lost(reason: str) -> None:
|
||||||
|
global _ume_subscription_lost, _ume_subscription_lost_reason
|
||||||
|
detail = str(reason or "").strip()[:500]
|
||||||
|
with _ume_lost_lock:
|
||||||
|
_ume_subscription_lost = True
|
||||||
|
_ume_subscription_lost_reason = detail
|
||||||
|
append_ws_log(f"UME 侧订阅已丢失: {detail[:200]}", level="warning")
|
||||||
|
request_ws_reconnect()
|
||||||
|
|
||||||
|
|
||||||
|
def clear_ume_subscription_lost_flag() -> None:
|
||||||
|
global _ume_subscription_lost, _ume_subscription_lost_reason
|
||||||
|
with _ume_lost_lock:
|
||||||
|
_ume_subscription_lost = False
|
||||||
|
_ume_subscription_lost_reason = ""
|
||||||
|
|
||||||
|
|
||||||
|
def is_ume_subscription_lost() -> bool:
|
||||||
|
with _ume_lost_lock:
|
||||||
|
return bool(_ume_subscription_lost)
|
||||||
|
|
||||||
|
|
||||||
|
def _server_subscription_lost_fields() -> dict[str, Any]:
|
||||||
|
with _ume_lost_lock:
|
||||||
|
lost = bool(_ume_subscription_lost)
|
||||||
|
reason = str(_ume_subscription_lost_reason or "")
|
||||||
|
return {
|
||||||
|
"server_subscription_lost": lost,
|
||||||
|
"server_subscription_lost_reason": reason,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _set_ws_connection_state(state: str, *, detail: str = "") -> None:
|
def _set_ws_connection_state(state: str, *, detail: str = "") -> None:
|
||||||
global _ws_connection_state, _ws_connection_detail
|
global _ws_connection_state, _ws_connection_detail
|
||||||
with _ws_connection_lock:
|
with _ws_connection_lock:
|
||||||
|
|
@ -227,15 +279,35 @@ def get_subscription_status() -> dict[str, Any]:
|
||||||
"subscription_id": sub_id,
|
"subscription_id": sub_id,
|
||||||
"wss_uri": uri,
|
"wss_uri": uri,
|
||||||
"topic": topic,
|
"topic": topic,
|
||||||
|
**_server_subscription_lost_fields(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def clear_local_alarm_subscription_manual(db: Session) -> dict[str, Any]:
|
||||||
|
"""Drop persisted/in-memory subscription without calling UME delete."""
|
||||||
|
clear_subscription(db, cache_key=DEFAULT_SUBSCRIPTION_KEY)
|
||||||
|
db.commit()
|
||||||
|
_clear_active_subscription()
|
||||||
|
clear_ume_subscription_lost_flag()
|
||||||
|
request_ws_reconnect()
|
||||||
|
append_ws_log("cleared local subscription record (UME delete skipped)")
|
||||||
|
return get_subscription_status()
|
||||||
|
|
||||||
|
|
||||||
def request_ws_reconnect() -> None:
|
def request_ws_reconnect() -> None:
|
||||||
_ws_wake_event.set()
|
_ws_wake_event.set()
|
||||||
|
|
||||||
|
|
||||||
def establish_alarm_subscription_manual(client: UMEClient, db: Session) -> dict[str, Any]:
|
def establish_alarm_subscription_manual(
|
||||||
|
client: UMEClient,
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
force_reestablish: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Manually establish ALARM subscription (UME API + persist). Does not open WSS."""
|
"""Manually establish ALARM subscription (UME API + persist). Does not open WSS."""
|
||||||
|
if force_reestablish or is_ume_subscription_lost():
|
||||||
|
clear_local_alarm_subscription_manual(db)
|
||||||
|
|
||||||
existing = load_subscription(db)
|
existing = load_subscription(db)
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
sub_id, uri, topic = existing
|
sub_id, uri, topic = existing
|
||||||
|
|
@ -284,27 +356,62 @@ def establish_alarm_subscription_manual(client: UMEClient, db: Session) -> dict[
|
||||||
with _shutdown_lock:
|
with _shutdown_lock:
|
||||||
_active_client = client
|
_active_client = client
|
||||||
_set_active_subscription(sub_id, uri, topic=topic)
|
_set_active_subscription(sub_id, uri, topic=topic)
|
||||||
|
clear_ume_subscription_lost_flag()
|
||||||
request_ws_reconnect()
|
request_ws_reconnect()
|
||||||
append_ws_log(f"manual establish subscription id={sub_id}", subscription_id=sub_id)
|
append_ws_log(f"manual establish subscription id={sub_id}", subscription_id=sub_id)
|
||||||
return {**get_subscription_status(), "already_exists": False}
|
return {**get_subscription_status(), "already_exists": False}
|
||||||
|
|
||||||
|
|
||||||
def cancel_alarm_subscription_manual(client: UMEClient, db: Session) -> dict[str, Any]:
|
def cancel_alarm_subscription_manual(
|
||||||
|
client: UMEClient,
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
force_clear_local: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Manually cancel subscription (UME delete + clear DB + drop WSS)."""
|
"""Manually cancel subscription (UME delete + clear DB + drop WSS)."""
|
||||||
loaded = load_subscription(db)
|
loaded = load_subscription(db)
|
||||||
sub_id, _uri = get_active_subscription()
|
sub_id, _uri = get_active_subscription()
|
||||||
if not sub_id and loaded is not None:
|
if not sub_id and loaded is not None:
|
||||||
sub_id = str(loaded[0] or "")
|
sub_id = str(loaded[0] or "")
|
||||||
|
ume_already_missing = False
|
||||||
if sub_id:
|
if sub_id:
|
||||||
client.refresh_if_needed()
|
client.refresh_if_needed()
|
||||||
_delete_subscription_on_ume(client, sub_id)
|
try:
|
||||||
|
_delete_subscription_on_ume(client, sub_id)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
if is_ume_subscription_missing_error(str(exc)):
|
||||||
|
ume_already_missing = True
|
||||||
|
mark_ume_subscription_lost(str(exc))
|
||||||
|
if not force_clear_local:
|
||||||
|
return {
|
||||||
|
**get_subscription_status(),
|
||||||
|
"ok": False,
|
||||||
|
"ume_already_missing": True,
|
||||||
|
"needs_local_cleanup": True,
|
||||||
|
"message": (
|
||||||
|
"UME 侧告警订阅已不存在或已过期(服务器订阅已丢失)。"
|
||||||
|
"请确认是否清除本地订阅记录,然后重新建立订阅。"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
clear_subscription(db, cache_key=DEFAULT_SUBSCRIPTION_KEY)
|
clear_subscription(db, cache_key=DEFAULT_SUBSCRIPTION_KEY)
|
||||||
db.commit()
|
db.commit()
|
||||||
_clear_active_subscription()
|
_clear_active_subscription()
|
||||||
|
clear_ume_subscription_lost_flag()
|
||||||
request_ws_reconnect()
|
request_ws_reconnect()
|
||||||
append_ws_log(f"manual cancel subscription id={sub_id or '(none)'}", subscription_id=sub_id)
|
append_ws_log(
|
||||||
return get_subscription_status()
|
f"manual cancel subscription id={sub_id or '(none)'}",
|
||||||
|
subscription_id=sub_id,
|
||||||
|
)
|
||||||
|
st = get_subscription_status()
|
||||||
|
return {
|
||||||
|
**st,
|
||||||
|
"ok": True,
|
||||||
|
"cleared_local": True,
|
||||||
|
"ume_already_missing": ume_already_missing,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _run_ws_session(
|
def _run_ws_session(
|
||||||
|
|
@ -357,14 +464,25 @@ def _run_ws_session(
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
def _on_error(_ws: Any, error: Any) -> None:
|
def _on_error(_ws: Any, error: Any) -> None:
|
||||||
detail = f"ws error: {str(error)[:200]}"
|
err_text = str(error)
|
||||||
_notify_ws_connection(
|
detail = f"ws error: {err_text[:200]}"
|
||||||
"error",
|
if is_ume_subscription_missing_error(err_text):
|
||||||
detail=detail,
|
mark_ume_subscription_lost(err_text)
|
||||||
subscription_id=subscription_id,
|
_notify_ws_connection(
|
||||||
on_status=on_status,
|
"subscription_lost",
|
||||||
log_level="error",
|
detail=detail,
|
||||||
)
|
subscription_id=subscription_id,
|
||||||
|
on_status=on_status,
|
||||||
|
log_level="warning",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_notify_ws_connection(
|
||||||
|
"error",
|
||||||
|
detail=detail,
|
||||||
|
subscription_id=subscription_id,
|
||||||
|
on_status=on_status,
|
||||||
|
log_level="error",
|
||||||
|
)
|
||||||
|
|
||||||
def _on_close(_ws: Any, close_status_code: Any, close_msg: Any) -> None:
|
def _on_close(_ws: Any, close_status_code: Any, close_msg: Any) -> None:
|
||||||
detail = f"ws closed code={close_status_code} msg={str(close_msg or '')[:120]}"
|
detail = f"ws closed code={close_status_code} msg={str(close_msg or '')[:120]}"
|
||||||
|
|
@ -461,6 +579,18 @@ def run_alarm_ws_consumer_loop(
|
||||||
time.sleep(1.0)
|
time.sleep(1.0)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if is_ume_subscription_lost():
|
||||||
|
with _ume_lost_lock:
|
||||||
|
lost_detail = str(_ume_subscription_lost_reason or "")
|
||||||
|
_loop_status(
|
||||||
|
"subscription_lost",
|
||||||
|
detail=lost_detail or "UME subscription missing on server",
|
||||||
|
log_level="warning",
|
||||||
|
)
|
||||||
|
_ws_wake_event.wait(timeout=5.0)
|
||||||
|
_ws_wake_event.clear()
|
||||||
|
continue
|
||||||
|
|
||||||
subscription_id, wss_uri = get_active_subscription()
|
subscription_id, wss_uri = get_active_subscription()
|
||||||
if not subscription_id or not wss_uri:
|
if not subscription_id or not wss_uri:
|
||||||
_loop_status("no_subscription")
|
_loop_status("no_subscription")
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,14 @@ from netx_api.ume_alarm_subscription_store import clear_subscription, load_subsc
|
||||||
from netx_api.ume_alarm_ws import (
|
from netx_api.ume_alarm_ws import (
|
||||||
append_ws_log,
|
append_ws_log,
|
||||||
cancel_alarm_subscription_manual,
|
cancel_alarm_subscription_manual,
|
||||||
|
clear_local_alarm_subscription_manual,
|
||||||
establish_alarm_subscription_manual,
|
establish_alarm_subscription_manual,
|
||||||
get_subscription_status,
|
get_subscription_status,
|
||||||
get_ws_connection_status,
|
get_ws_connection_status,
|
||||||
get_ws_logs,
|
get_ws_logs,
|
||||||
|
is_ume_subscription_missing_error,
|
||||||
load_persisted_subscription,
|
load_persisted_subscription,
|
||||||
|
mark_ume_subscription_lost,
|
||||||
parse_subscription_id_from_already_exists_error,
|
parse_subscription_id_from_already_exists_error,
|
||||||
process_alarm_notification,
|
process_alarm_notification,
|
||||||
)
|
)
|
||||||
|
|
@ -739,6 +742,53 @@ class UmeAlarmNotificationTests(unittest.TestCase):
|
||||||
self.assertTrue(changed)
|
self.assertTrue(changed)
|
||||||
self.assertIsNone(self.db.get(UmeAlarmCurrent, "AK-CLEARED-1"))
|
self.assertIsNone(self.db.get(UmeAlarmCurrent, "AK-CLEARED-1"))
|
||||||
|
|
||||||
|
def test_is_ume_subscription_missing_error(self):
|
||||||
|
err = (
|
||||||
|
"ConnectFailed: Server responded with a non-101 status: 503 "
|
||||||
|
"status-reason: Subscription not found or overtime! Please establish again"
|
||||||
|
)
|
||||||
|
self.assertTrue(is_ume_subscription_missing_error(err))
|
||||||
|
self.assertTrue(
|
||||||
|
is_ume_subscription_missing_error(
|
||||||
|
'ume_request_failed:DELETE ... 400:{"errorInfo":"subscription not exist!"}'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_cancel_returns_needs_cleanup_when_ume_missing(self):
|
||||||
|
from time import time as _time
|
||||||
|
|
||||||
|
ume_alarm_ws._clear_active_subscription()
|
||||||
|
ume_alarm_ws.clear_ume_subscription_lost_flag()
|
||||||
|
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
|
||||||
|
TestingSessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=False)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
db = TestingSessionLocal()
|
||||||
|
|
||||||
|
client = UMEClient(base_url="https://ume.local:18014", username="u", password="p", verify_tls=False)
|
||||||
|
client._token_value = "token-1"
|
||||||
|
client._token_expires_at = _time() + 3600
|
||||||
|
save_subscription(db, subscription_id="sub-gone", wss_uri="wss://ume.local/stream/sub-gone", topic="ALARM")
|
||||||
|
db.commit()
|
||||||
|
ume_alarm_ws._set_active_subscription("sub-gone", "wss://ume.local/stream/sub-gone")
|
||||||
|
|
||||||
|
def _fail_delete(_id: str) -> None:
|
||||||
|
raise RuntimeError('ume_request_failed:DELETE ... 400:{"errorInfo":"subscription not exist!"}')
|
||||||
|
|
||||||
|
client.delete_alarm_subscription = _fail_delete # type: ignore[method-assign]
|
||||||
|
client.refresh_if_needed = lambda: "token-1" # type: ignore[method-assign]
|
||||||
|
|
||||||
|
st = cancel_alarm_subscription_manual(client, db)
|
||||||
|
self.assertFalse(st.get("ok", True))
|
||||||
|
self.assertTrue(st.get("needs_local_cleanup"))
|
||||||
|
self.assertTrue(st.get("active"))
|
||||||
|
self.assertTrue(ume_alarm_ws.is_ume_subscription_lost())
|
||||||
|
|
||||||
|
st2 = cancel_alarm_subscription_manual(client, db, force_clear_local=True)
|
||||||
|
self.assertTrue(st2.get("ok"))
|
||||||
|
self.assertFalse(st2.get("active"))
|
||||||
|
self.assertFalse(ume_alarm_ws.is_ume_subscription_lost())
|
||||||
|
db.close()
|
||||||
|
|
||||||
def test_ws_connection_status_not_alarm_action(self):
|
def test_ws_connection_status_not_alarm_action(self):
|
||||||
from netx_api import ume_alarm_ws as ws_mod
|
from netx_api import ume_alarm_ws as ws_mod
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
fetchUmeCurrentAlarms,
|
fetchUmeCurrentAlarms,
|
||||||
fetchUmeNe,
|
fetchUmeNe,
|
||||||
cancelUmeAlarmSubscription,
|
cancelUmeAlarmSubscription,
|
||||||
|
clearLocalUmeAlarmSubscription,
|
||||||
establishUmeAlarmSubscription,
|
establishUmeAlarmSubscription,
|
||||||
fetchUmeAlarmSubscriptionStatus,
|
fetchUmeAlarmSubscriptionStatus,
|
||||||
fetchUmeSyncStatus,
|
fetchUmeSyncStatus,
|
||||||
|
|
@ -89,8 +90,13 @@ export function UmePage({ toastOk, toastError }: UmePageProps) {
|
||||||
staleTime: 3000,
|
staleTime: 3000,
|
||||||
refetchInterval: 5000,
|
refetchInterval: 5000,
|
||||||
});
|
});
|
||||||
|
const confirmClearLocalSubscription = (hint?: string) =>
|
||||||
|
window.confirm(
|
||||||
|
`${hint || "UME 侧告警订阅已不存在或已过期(服务器订阅已丢失)。"}\n\n是否清除本地订阅记录?清除后可点击「建立告警订阅」重新订阅。`,
|
||||||
|
);
|
||||||
|
|
||||||
const subscriptionEstablishMutation = useMutation({
|
const subscriptionEstablishMutation = useMutation({
|
||||||
mutationFn: establishUmeAlarmSubscription,
|
mutationFn: (opts?: { forceReestablish?: boolean }) => establishUmeAlarmSubscription(opts),
|
||||||
onMutate: () => setSubscriptionOpError(""),
|
onMutate: () => setSubscriptionOpError(""),
|
||||||
onSuccess: async (res) => {
|
onSuccess: async (res) => {
|
||||||
if (!res?.active) {
|
if (!res?.active) {
|
||||||
|
|
@ -114,17 +120,47 @@ export function UmePage({ toastOk, toastError }: UmePageProps) {
|
||||||
toastError?.(msg);
|
toastError?.(msg);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const subscriptionClearLocalMutation = useMutation({
|
||||||
|
mutationFn: clearLocalUmeAlarmSubscription,
|
||||||
|
onMutate: () => setSubscriptionOpError(""),
|
||||||
|
onSuccess: async () => {
|
||||||
|
setSubscriptionOpError("");
|
||||||
|
toastOk?.("已清除本地订阅记录,可重新建立订阅");
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["umeAlarmSubscription"] });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["umeSyncStatus"] });
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
const msg = String(err);
|
||||||
|
setSubscriptionOpError(msg);
|
||||||
|
toastError?.(msg);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const subscriptionCancelMutation = useMutation({
|
const subscriptionCancelMutation = useMutation({
|
||||||
mutationFn: cancelUmeAlarmSubscription,
|
mutationFn: (opts?: { forceClearLocal?: boolean }) => cancelUmeAlarmSubscription(opts),
|
||||||
onMutate: () => setSubscriptionOpError(""),
|
onMutate: () => setSubscriptionOpError(""),
|
||||||
onSuccess: async (res) => {
|
onSuccess: async (res) => {
|
||||||
|
if (res?.needs_local_cleanup) {
|
||||||
|
const msg =
|
||||||
|
res.message ||
|
||||||
|
"UME 侧告警订阅已丢失。请确认是否清除本地订阅记录后重新订阅。";
|
||||||
|
if (confirmClearLocalSubscription(msg)) {
|
||||||
|
subscriptionCancelMutation.mutate({ forceClearLocal: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSubscriptionOpError(msg);
|
||||||
|
toastError?.(msg);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["umeAlarmSubscription"] });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["umeSyncStatus"] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (res?.active) {
|
if (res?.active) {
|
||||||
const msg = "取消订阅失败:UME 侧订阅可能仍存在,请查看错误详情后重试";
|
const msg = "取消订阅失败:UME 侧订阅可能仍存在,请查看错误详情后重试";
|
||||||
setSubscriptionOpError(msg);
|
setSubscriptionOpError(msg);
|
||||||
toastError?.(msg);
|
toastError?.(msg);
|
||||||
} else {
|
} else {
|
||||||
setSubscriptionOpError("");
|
setSubscriptionOpError("");
|
||||||
toastOk?.("告警订阅已取消");
|
toastOk?.(res?.ume_already_missing ? "本地已清除(UME 侧订阅此前已不存在)" : "告警订阅已取消");
|
||||||
}
|
}
|
||||||
await queryClient.invalidateQueries({ queryKey: ["umeAlarmSubscription"] });
|
await queryClient.invalidateQueries({ queryKey: ["umeAlarmSubscription"] });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["umeSyncStatus"] });
|
await queryClient.invalidateQueries({ queryKey: ["umeSyncStatus"] });
|
||||||
|
|
@ -204,21 +240,33 @@ export function UmePage({ toastOk, toastError }: UmePageProps) {
|
||||||
wsConn?.label ||
|
wsConn?.label ||
|
||||||
(wsPaused ? "已暂停" : wsConsumer?.last_error || wsConsumer?.status || "-");
|
(wsPaused ? "已暂停" : wsConsumer?.last_error || wsConsumer?.status || "-");
|
||||||
const wsPillLevel =
|
const wsPillLevel =
|
||||||
wsPaused || wsState === "paused"
|
serverSubLost || wsState === "subscription_lost"
|
||||||
? "warn"
|
? "warn"
|
||||||
: wsState === "connected"
|
: wsPaused || wsState === "paused"
|
||||||
? "up"
|
? "warn"
|
||||||
: wsState === "connecting" || wsState === "reconnecting" || wsState === "waiting_token"
|
: wsState === "connected"
|
||||||
? "warn"
|
? "up"
|
||||||
: wsState === "no_subscription" || wsState === "disconnected" || wsState === "error" || wsState === "init"
|
: wsState === "connecting" || wsState === "reconnecting" || wsState === "waiting_token"
|
||||||
? "down"
|
? "warn"
|
||||||
: String(wsConsumer?.last_error || "").includes("connected")
|
: wsState === "no_subscription" || wsState === "disconnected" || wsState === "error" || wsState === "init"
|
||||||
? "up"
|
? "down"
|
||||||
: "unknown";
|
: String(wsConsumer?.last_error || "").includes("connected")
|
||||||
|
? "up"
|
||||||
|
: "unknown";
|
||||||
const wsLogs = [...(subscriptionStatusQuery.data?.ws_logs ?? alarmSub.ws_logs ?? [])].reverse();
|
const wsLogs = [...(subscriptionStatusQuery.data?.ws_logs ?? alarmSub.ws_logs ?? [])].reverse();
|
||||||
const subscriptionActive = Boolean(alarmSub.active);
|
const subscriptionActive = Boolean(alarmSub.active);
|
||||||
|
const serverSubLost = Boolean(
|
||||||
|
subscriptionStatusQuery.data?.server_subscription_lost ?? alarmSub.server_subscription_lost,
|
||||||
|
);
|
||||||
|
const serverSubLostReason = String(
|
||||||
|
subscriptionStatusQuery.data?.server_subscription_lost_reason ??
|
||||||
|
alarmSub.server_subscription_lost_reason ??
|
||||||
|
"",
|
||||||
|
);
|
||||||
const subPending =
|
const subPending =
|
||||||
subscriptionEstablishMutation.isPending || subscriptionCancelMutation.isPending;
|
subscriptionEstablishMutation.isPending ||
|
||||||
|
subscriptionCancelMutation.isPending ||
|
||||||
|
subscriptionClearLocalMutation.isPending;
|
||||||
|
|
||||||
const runtimeTaskMutation = useMutation({
|
const runtimeTaskMutation = useMutation({
|
||||||
mutationFn: async (vars: { task: string; action: "pause" | "resume" }) =>
|
mutationFn: async (vars: { task: string; action: "pause" | "resume" }) =>
|
||||||
|
|
@ -336,22 +384,72 @@ export function UmePage({ toastOk, toastError }: UmePageProps) {
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
{serverSubLost ? (
|
||||||
|
<div className="pill pill--medium" style={{ marginTop: 8 }}>
|
||||||
|
UME 服务器侧告警订阅已丢失或已过期,本地记录可能仍显示「已建立」。请确认清除本地记录后重新订阅。
|
||||||
|
{serverSubLostReason ? (
|
||||||
|
<div className="muted" style={{ marginTop: 6, fontSize: 12, wordBreak: "break-word" }}>
|
||||||
|
{serverSubLostReason.slice(0, 280)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="actions-row actions-row--inline">
|
<div className="actions-row actions-row--inline">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => subscriptionEstablishMutation.mutate()}
|
onClick={() => {
|
||||||
disabled={subPending || subscriptionActive || !hasToken}
|
if (serverSubLost && subscriptionActive) {
|
||||||
title={!hasToken ? "请先登录 UME token" : undefined}
|
if (
|
||||||
|
window.confirm(
|
||||||
|
"将清除本地订阅记录并在 UME 上重新建立告警订阅,是否继续?",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
subscriptionEstablishMutation.mutate({ forceReestablish: true });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
subscriptionEstablishMutation.mutate();
|
||||||
|
}}
|
||||||
|
disabled={subPending || (!serverSubLost && subscriptionActive) || !hasToken}
|
||||||
|
title={
|
||||||
|
!hasToken
|
||||||
|
? "请先登录 UME token"
|
||||||
|
: serverSubLost
|
||||||
|
? "清除本地失效记录并重新向 UME 建立订阅"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{subscriptionEstablishMutation.isPending ? (
|
{subscriptionEstablishMutation.isPending ? (
|
||||||
<>
|
<>
|
||||||
<span className="inline-spinner" aria-hidden />
|
<span className="inline-spinner" aria-hidden />
|
||||||
建立订阅…
|
{serverSubLost ? "重新订阅…" : "建立订阅…"}
|
||||||
</>
|
</>
|
||||||
|
) : serverSubLost ? (
|
||||||
|
"清除本地并重新订阅"
|
||||||
) : (
|
) : (
|
||||||
"建立告警订阅"
|
"建立告警订阅"
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
{serverSubLost && subscriptionActive ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirmClearLocalSubscription()) {
|
||||||
|
subscriptionClearLocalMutation.mutate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={subPending}
|
||||||
|
>
|
||||||
|
{subscriptionClearLocalMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<span className="inline-spinner" aria-hidden />
|
||||||
|
清除中…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"仅清除本地记录"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => subscriptionCancelMutation.mutate()}
|
onClick={() => subscriptionCancelMutation.mutate()}
|
||||||
|
|
|
||||||
|
|
@ -104,11 +104,18 @@ export const fetchAlarms = (params: {
|
||||||
export const fetchUmeAlarmSubscriptionStatus = () =>
|
export const fetchUmeAlarmSubscriptionStatus = () =>
|
||||||
apiGet<UmeAlarmSubscriptionStatus>("/v1/ume/alarm-subscription/status");
|
apiGet<UmeAlarmSubscriptionStatus>("/v1/ume/alarm-subscription/status");
|
||||||
|
|
||||||
export const establishUmeAlarmSubscription = () =>
|
export const establishUmeAlarmSubscription = (opts?: { forceReestablish?: boolean }) =>
|
||||||
apiPost<UmeAlarmSubscriptionStatus>("/v1/ume/alarm-subscription/establish", {});
|
apiPost<UmeAlarmSubscriptionStatus>("/v1/ume/alarm-subscription/establish", {
|
||||||
|
force_reestablish: Boolean(opts?.forceReestablish),
|
||||||
|
});
|
||||||
|
|
||||||
export const cancelUmeAlarmSubscription = () =>
|
export const cancelUmeAlarmSubscription = (opts?: { forceClearLocal?: boolean }) =>
|
||||||
apiPost<UmeAlarmSubscriptionStatus>("/v1/ume/alarm-subscription/cancel", {});
|
apiPost<UmeAlarmSubscriptionStatus>("/v1/ume/alarm-subscription/cancel", {
|
||||||
|
force_clear_local: Boolean(opts?.forceClearLocal),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clearLocalUmeAlarmSubscription = () =>
|
||||||
|
apiPost<UmeAlarmSubscriptionStatus>("/v1/ume/alarm-subscription/clear-local", {});
|
||||||
|
|
||||||
export const fetchUmeSyncStatus = (params: { page: number; pageSize: number }) => {
|
export const fetchUmeSyncStatus = (params: { page: number; pageSize: number }) => {
|
||||||
const p = new URLSearchParams();
|
const p = new URLSearchParams();
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,12 @@ export type UmeAlarmSubscriptionStatus = {
|
||||||
subscription_id?: string;
|
subscription_id?: string;
|
||||||
wss_uri?: string;
|
wss_uri?: string;
|
||||||
topic?: string;
|
topic?: string;
|
||||||
|
server_subscription_lost?: boolean;
|
||||||
|
server_subscription_lost_reason?: string;
|
||||||
|
needs_local_cleanup?: boolean;
|
||||||
|
ume_already_missing?: boolean;
|
||||||
|
cleared_local?: boolean;
|
||||||
|
message?: string;
|
||||||
ws_connection?: UmeWsConnectionStatus;
|
ws_connection?: UmeWsConnectionStatus;
|
||||||
ws_consumer_status?: string;
|
ws_consumer_status?: string;
|
||||||
ws_consumer_last_error?: string;
|
ws_consumer_last_error?: string;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue