Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

Commit bcf29ba

Browse files
committed
Run update call on recurring schedule
Call to the update service every four hours, and use the BE origin type. Print a warning level log message if an update is available. This PR also introduces some refactoring from the previous PR: 1) Refactor the update client to be a singleton. 2) Set the instance ID once on application load. 3) Get rid of the feature flag - using the new service is now default.
1 parent 17731aa commit bcf29ba

File tree

8 files changed

+81
-52
lines changed

8 files changed

+81
-52
lines changed

src/codegate/api/v1.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import List, Optional
22
from uuid import UUID
33

4+
import cachetools.func
45
import requests
56
import structlog
67
from fastapi import APIRouter, Depends, HTTPException, Query, Response
@@ -10,7 +11,7 @@
1011

1112
from codegate.config import API_DEFAULT_PAGE_SIZE, API_MAX_PAGE_SIZE
1213
import codegate.muxing.models as mux_models
13-
from codegate import Config, __version__
14+
from codegate import __version__
1415
from codegate.api import v1_models, v1_processing
1516
from codegate.db.connection import AlreadyExistsError, DbReader
1617
from codegate.db.models import AlertSeverity, AlertTriggerType, Persona, WorkspaceWithModel
@@ -20,7 +21,7 @@
2021
PersonaSimilarDescriptionError,
2122
)
2223
from codegate.providers import crud as provendcrud
23-
from codegate.updates.client import Origin, UpdateClient
24+
from codegate.updates.client import Origin, UpdateClient, get_update_client_singleton
2425
from codegate.workspaces import crud
2526

2627
logger = structlog.get_logger("codegate")
@@ -32,7 +33,6 @@
3233

3334
# This is a singleton object
3435
dbreader = DbReader()
35-
update_client = UpdateClient(Config.get_config().update_service_url, __version__, dbreader)
3636

3737

3838
def uniq_name(route: APIRoute):
@@ -728,10 +728,7 @@ async def stream_sse():
728728
@v1.get("/version", tags=["Dashboard"], generate_unique_id_function=uniq_name)
729729
async def version_check():
730730
try:
731-
if Config.get_config().use_update_service:
732-
latest_version = await update_client.get_latest_version(Origin.FrontEnd)
733-
else:
734-
latest_version = v1_processing.fetch_latest_version()
731+
latest_version = __get_latest_version()
735732
# normalize the versions as github will return them with a 'v' prefix
736733
current_version = __version__.lstrip("v")
737734
latest_version_stripped = latest_version.lstrip("v")
@@ -885,3 +882,8 @@ async def delete_persona(persona_name: str):
885882
except Exception:
886883
logger.exception("Error while deleting persona")
887884
raise HTTPException(status_code=500, detail="Internal server error")
885+
886+
@cachetools.func.ttl_cache(maxsize=128, ttl=20 * 60)
887+
def __get_latest_version():
888+
update_client = get_update_client_singleton()
889+
return update_client.get_latest_version(Origin.FrontEnd)

src/codegate/api/v1_processing.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
from collections import defaultdict
44
from typing import AsyncGenerator, Dict, List, Optional, Tuple
55

6-
import cachetools.func
76
import regex as re
8-
import requests
97
import structlog
108

119
from codegate.api import v1_models
@@ -34,16 +32,6 @@
3432
]
3533

3634

37-
@cachetools.func.ttl_cache(maxsize=128, ttl=20 * 60)
38-
def fetch_latest_version() -> str:
39-
url = "https://api.github.com/repos/stacklok/codegate/releases/latest"
40-
headers = {"Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
41-
response = requests.get(url, headers=headers, timeout=5)
42-
response.raise_for_status()
43-
data = response.json()
44-
return data.get("tag_name", "unknown")
45-
46-
4735
async def generate_sse_events() -> AsyncGenerator[str, None]:
4836
"""
4937
SSE generator from queue

src/codegate/cli.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from uvicorn.config import Config as UvicornConfig
1212
from uvicorn.server import Server
1313

14+
import codegate
1415
from codegate.ca.codegate_ca import CertificateAuthority
1516
from codegate.codegate_logging import LogFormat, LogLevel, setup_logging
1617
from codegate.config import Config, ConfigurationError
@@ -25,6 +26,8 @@
2526
from codegate.providers.copilot.provider import CopilotProvider
2627
from codegate.server import init_app
2728
from codegate.storage.utils import restore_storage_backup
29+
from codegate.updates.client import init_update_client_singleton
30+
from codegate.updates.scheduled import ScheduledUpdateChecker
2831
from codegate.workspaces import crud as wscrud
2932

3033

@@ -322,9 +325,16 @@ def serve( # noqa: C901
322325
logger = structlog.get_logger("codegate").bind(origin="cli")
323326

324327
init_db_sync(cfg.db_path)
325-
init_instance(cfg.db_path)
328+
instance_id = init_instance(cfg.db_path)
326329
init_session_if_not_exists(cfg.db_path)
327330

331+
# Initialize the update checking logic.
332+
update_client = init_update_client_singleton(cfg.update_service_url, codegate.__version__, instance_id)
333+
update_checker = ScheduledUpdateChecker(update_client)
334+
update_checker.daemon = True
335+
update_checker.run()
336+
update_checker.start()
337+
328338
# Check certificates and create CA if necessary
329339
logger.info("Checking certificates and creating CA if needed")
330340
ca = CertificateAuthority.get_instance()

src/codegate/clients/__init__.py

Whitespace-only changes.

src/codegate/config.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,6 @@ def from_env(cls) -> "Config":
220220
config.db_path = os.environ["CODEGATE_DB_PATH"]
221221
if "CODEGATE_VEC_DB_PATH" in os.environ:
222222
config.vec_db_path = os.environ["CODEGATE_VEC_DB_PATH"]
223-
if "CODEGATE_USE_UPDATE_SERVICE" in os.environ:
224-
config.use_update_service = cls.__bool_from_string(
225-
os.environ["CODEGATE_USE_UPDATE_SERVICE"]
226-
)
227223
if "CODEGATE_UPDATE_SERVICE_URL" in os.environ:
228224
config.update_service_url = os.environ["CODEGATE_UPDATE_SERVICE_URL"]
229225

@@ -258,7 +254,6 @@ def load(
258254
force_certs: Optional[bool] = None,
259255
db_path: Optional[str] = None,
260256
vec_db_path: Optional[str] = None,
261-
use_update_service: Optional[bool] = None,
262257
update_service_url: Optional[str] = None,
263258
) -> "Config":
264259
"""Load configuration with priority resolution.
@@ -288,7 +283,6 @@ def load(
288283
force_certs: Optional flag to force certificate generation
289284
db_path: Optional path to the main SQLite database file
290285
vec_db_path: Optional path to the vector SQLite database file
291-
use_update_service: Optional flag to enable the update service
292286
update_service_url: Optional URL for the update service
293287
294288
Returns:
@@ -342,8 +336,6 @@ def load(
342336
config.db_path = env_config.db_path
343337
if "CODEGATE_VEC_DB_PATH" in os.environ:
344338
config.vec_db_path = env_config.vec_db_path
345-
if "CODEGATE_USE_UPDATE_SERVICE" in os.environ:
346-
config.use_update_service = env_config.use_update_service
347339
if "CODEGATE_UPDATE_SERVICE_URL" in os.environ:
348340
config.update_service_url = env_config.update_service_url
349341

@@ -386,8 +378,6 @@ def load(
386378
config.vec_db_path = vec_db_path
387379
if force_certs is not None:
388380
config.force_certs = force_certs
389-
if use_update_service is not None:
390-
config.use_update_service = use_update_service
391381
if update_service_url is not None:
392382
config.update_service_url = update_service_url
393383

src/codegate/db/connection.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -600,10 +600,11 @@ async def delete_persona(self, persona_id: str) -> None:
600600
conditions = {"id": persona_id}
601601
await self._execute_with_no_return(sql, conditions)
602602

603-
async def init_instance(self) -> None:
603+
async def init_instance(self) -> str:
604604
"""
605605
Initializes instance details in the database.
606606
"""
607+
instance_id = str(uuid.uuid4())
607608
sql = text(
608609
"""
609610
INSERT INTO instance (id, created_at)
@@ -613,13 +614,14 @@ async def init_instance(self) -> None:
613614

614615
try:
615616
instance = Instance(
616-
id=str(uuid.uuid4()),
617+
id=instance_id,
617618
created_at=datetime.datetime.now(datetime.timezone.utc),
618619
)
619620
await self._execute_with_no_return(sql, instance.model_dump())
620621
except IntegrityError as e:
621622
logger.debug(f"Exception type: {type(e)}")
622623
raise AlreadyExistsError("Instance already initialized.")
624+
return instance_id
623625

624626

625627
class DbReader(DbCodeGate):
@@ -1326,18 +1328,21 @@ def init_session_if_not_exists(db_path: Optional[str] = None):
13261328
logger.info("Session in DB initialized successfully.")
13271329

13281330

1329-
def init_instance(db_path: Optional[str] = None):
1331+
def init_instance(db_path: Optional[str] = None) -> str:
13301332
db_reader = DbReader(db_path)
13311333
instance = asyncio.run(db_reader.get_instance())
13321334
# Initialize instance if not already initialized.
13331335
if not instance:
13341336
db_recorder = DbRecorder(db_path)
13351337
try:
1336-
asyncio.run(db_recorder.init_instance())
1338+
instance_id = asyncio.run(db_recorder.init_instance())
1339+
logger.info("Instance initialized successfully.")
1340+
return instance_id
13371341
except Exception as e:
13381342
logger.error(f"Failed to initialize instance in DB: {e}")
13391343
raise
1340-
logger.info("Instance initialized successfully.")
1344+
else:
1345+
return instance[0].id
13411346

13421347

13431348
if __name__ == "__main__":

src/codegate/updates/client.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,26 @@
99
logger = structlog.get_logger("codegate")
1010

1111

12+
__update_client_singleton = None
13+
1214
# Enum representing whether the request is coming from the front-end or the back-end.
1315
class Origin(Enum):
1416
FrontEnd = "FE"
1517
BackEnd = "BE"
1618

1719

1820
class UpdateClient:
19-
def __init__(self, update_url: str, current_version: str, db_reader: DbReader):
21+
def __init__(self, update_url: str, current_version: str, instance_id: str):
2022
self.__update_url = update_url
2123
self.__current_version = current_version
22-
self.__db_reader = db_reader
23-
self.__instance_id = None
24+
self.__instance_id = instance_id
2425

25-
async def get_latest_version(self, origin: Origin) -> str:
26+
def get_latest_version(self, origin: Origin) -> str:
2627
"""
2728
Retrieves the latest version of CodeGate from updates.codegate.ai
2829
"""
29-
logger.info(f"Fetching latest version from {self.__update_url}")
30-
instance_id = await self.__get_instance_id()
31-
return self.__fetch_latest_version(instance_id, origin)
32-
33-
@cachetools.func.ttl_cache(maxsize=128, ttl=20 * 60)
34-
def __fetch_latest_version(self, instance_id: str, origin: Origin) -> str:
3530
headers = {
36-
"X-Instance-ID": instance_id,
31+
"X-Instance-ID": self.__instance_id,
3732
"User-Agent": f"codegate/{self.__current_version} {origin.value}",
3833
}
3934

@@ -46,9 +41,15 @@ def __fetch_latest_version(self, instance_id: str, origin: Origin) -> str:
4641
logger.error(f"Error fetching latest version from f{self.__update_url}: {e}")
4742
return "unknown"
4843

49-
# Lazy load the instance ID from the DB.
50-
async def __get_instance_id(self):
51-
if self.__instance_id is None:
52-
instance_data = await self.__db_reader.get_instance()
53-
self.__instance_id = instance_data[0].id
54-
return self.__instance_id
44+
# Use a singleton since we do not have a good way of doing dependency injection
45+
# with the API endpoints.
46+
def init_update_client_singleton(update_url: str, current_version: str, instance_id: str) -> UpdateClient:
47+
global __update_client_singleton
48+
__update_client_singleton = UpdateClient(update_url, current_version, instance_id)
49+
return __update_client_singleton
50+
51+
def get_update_client_singleton() -> UpdateClient:
52+
global __update_client_singleton
53+
if __update_client_singleton is None:
54+
raise ValueError("UpdateClient singleton not initialized")
55+
return __update_client_singleton

src/codegate/updates/scheduled.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import time
2+
3+
import structlog
4+
import threading
5+
6+
import codegate
7+
from codegate.updates.client import UpdateClient, Origin
8+
9+
logger = structlog.get_logger("codegate")
10+
11+
12+
class ScheduledUpdateChecker(threading.Thread):
13+
"""
14+
ScheduledUpdateChecker calls the UpdateClient on a recurring interval.
15+
This is implemented as a separate thread to avoid blocking the main thread.
16+
A dedicated scheduling library could have been used, but the requirements
17+
are trivial, and a simple hand-rolled solution is sufficient.
18+
"""
19+
def __init__(self, client: UpdateClient, interval_seconds: int = 14400): # 4 hours in seconds
20+
super().__init__()
21+
self.__client = client
22+
self.__interval_seconds = interval_seconds
23+
24+
def run(self):
25+
"""
26+
Overrides the `run` method of threading.Thread.
27+
"""
28+
while True:
29+
logger.info("Checking for CodeGate updates")
30+
latest = self.__client.get_latest_version(Origin.BackEnd)
31+
if latest != codegate.__version__:
32+
logger.warning(f"A new version of CodeGate is available: {latest}")
33+
time.sleep(self.__interval_seconds)

0 commit comments

Comments
 (0)