From 4a91b5dc991eeb3dc4e3b000e65f058a2819ac5f Mon Sep 17 00:00:00 2001 From: nsheff Date: Sat, 22 Aug 2026 16:16:44 -0400 Subject: [PATCH 1/2] Add /v1/usage endpoint and scheduler toggle --- bedhost/main.py | 35 +++++++++++++++++++++++++---------- bedhost/routers/base_api.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/bedhost/main.py b/bedhost/main.py index ae1fa816..ad69b18b 100644 --- a/bedhost/main.py +++ b/bedhost/main.py @@ -109,21 +109,36 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: _LOGGER.info("Initializing reference genome validator...") app.state.ref_validator = ReferenceValidator() - scheduler = BackgroundScheduler() - scheduler.add_job( - upload_usage, - "interval", - hours=USAGE_SAVE_HOURS, - args=(app.state.bbagent, app.state.usage_data), - ) - scheduler.start() - app.state.scheduler = scheduler + # Read-only guard: when BEDHOST_DISABLE_USAGE_SCHEDULER is truthy, do NOT + # start the hourly usage-flush job (which writes to the DB). Default (unset) + # keeps the scheduler ON so normal/prod deployments are unaffected. + if os.environ.get("BEDHOST_DISABLE_USAGE_SCHEDULER", "").lower() not in ( + "1", + "true", + "yes", + ): + scheduler = BackgroundScheduler() + scheduler.add_job( + upload_usage, + "interval", + hours=USAGE_SAVE_HOURS, + args=(app.state.bbagent, app.state.usage_data), + ) + scheduler.start() + app.state.scheduler = scheduler + else: + app.state.scheduler = None + _LOGGER.info( + "BEDHOST_DISABLE_USAGE_SCHEDULER set; usage flush scheduler NOT " + "started (read-only mode)." + ) try: _LOGGER.info("Starting app ...") yield finally: - app.state.scheduler.shutdown(wait=False) + if app.state.scheduler is not None: + app.state.scheduler.shutdown(wait=False) app = FastAPI( diff --git a/bedhost/routers/base_api.py b/bedhost/routers/base_api.py index 9b852788..7e3f8809 100644 --- a/bedhost/routers/base_api.py +++ b/bedhost/routers/base_api.py @@ -1,4 +1,6 @@ +import datetime from platform import python_version +from typing import Literal from bbconf import __version__ as bbconf_version from bbconf.bbagent import BedBaseAgent @@ -7,6 +9,7 @@ BedSnapshotListResult, FileStats, StatsReturn, + UsageResponse, UsageStats, ) from fastapi import APIRouter, Depends, Query, Request @@ -238,3 +241,32 @@ async def redirect_to_download( ): download_url = f"{EXPORTS_URL_BASE}{file_path}" return RedirectResponse(url=download_url) + + +@router.get( + "/usage", + summary="Get per-key usage counts for one event type, optionally by date range", + response_model=UsageResponse, +) +async def get_usage( + type: Literal["files", "bed_meta", "bedset_meta", "bed_search", "bedset_search"], + date_from: datetime.datetime | None = None, + date_to: datetime.datetime | None = None, + limit: int | None = None, + offset: int = 0, + bbagent: BedBaseAgent = Depends(get_bbagent), +): + """ + Returns per-key usage counts for a single event type + (files | bed_meta | bedset_meta | bed_search | bedset_search), grouped by key + and summed over the time-bucketed rows whose window overlaps the requested + [date_from, date_to] range. Both date bounds are optional; limit=None returns + all matching keys, otherwise limit/offset paginate the keys by count. + """ + return bbagent.get_usage( + event_type=type, + date_from=date_from, + date_to=date_to, + limit=limit, + offset=offset, + ) From d14ab491625eb6c3f1be8689198a84ce1fafb985 Mon Sep 17 00:00:00 2001 From: nsheff Date: Sat, 22 Aug 2026 16:47:01 -0400 Subject: [PATCH 2/2] Fix usage endpoint: sync def, default limit Change async def to def so FastAPI runs the blocking DB call in a threadpool; cap limit at 1000 to bound response size. --- bedhost/routers/base_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bedhost/routers/base_api.py b/bedhost/routers/base_api.py index 7e3f8809..3b911e05 100644 --- a/bedhost/routers/base_api.py +++ b/bedhost/routers/base_api.py @@ -248,11 +248,11 @@ async def redirect_to_download( summary="Get per-key usage counts for one event type, optionally by date range", response_model=UsageResponse, ) -async def get_usage( +def get_usage( type: Literal["files", "bed_meta", "bedset_meta", "bed_search", "bedset_search"], date_from: datetime.datetime | None = None, date_to: datetime.datetime | None = None, - limit: int | None = None, + limit: int = 1000, offset: int = 0, bbagent: BedBaseAgent = Depends(get_bbagent), ): @@ -260,8 +260,8 @@ async def get_usage( Returns per-key usage counts for a single event type (files | bed_meta | bedset_meta | bed_search | bedset_search), grouped by key and summed over the time-bucketed rows whose window overlaps the requested - [date_from, date_to] range. Both date bounds are optional; limit=None returns - all matching keys, otherwise limit/offset paginate the keys by count. + [date_from, date_to] range. Both date bounds are optional; limit/offset + paginate the keys by count (default limit 1000). """ return bbagent.get_usage( event_type=type,