1010from arango import ArangoServerError
1111from arango .client import ArangoClient
1212from arango .database import StandardDatabase
13- from attr import frozen
13+ from attr import frozen , evolve
1414from dateutil .parser import parse
1515from requests .exceptions import RequestException
1616
1717from fixcore .analytics import AnalyticsEventSender
1818from fixcore .async_extensions import run_async
1919from fixcore .core_config import CoreConfig , current_git_hash
20- from fixcore .db import SystemData
20+ from fixcore .db import SystemData , DatabaseChange
2121from fixcore .db .arangodb_extensions import ArangoHTTPClient
2222from fixcore .db .async_arangodb import AsyncArangoDB , AsyncCursor
2323from fixcore .db .configdb import config_entity_db , config_validation_entity_db
2424from fixcore .db .deferredouteredgedb import deferred_outer_edge_db
2525from fixcore .db .entitydb import EventEntityDb
2626from fixcore .db .graphdb import ArangoGraphDB , GraphDB , EventGraphDB
2727from fixcore .db .jobdb import job_db
28+ from fixcore .db .lockdb import LockDB
2829from fixcore .db .modeldb import ModelDb , model_db
2930from fixcore .db .packagedb import app_package_entity_db
3031from fixcore .db .reportdb import report_check_db , benchmark_db
@@ -76,6 +77,7 @@ def __init__(
7677 time_series : str = "ts" ,
7778 report_checks : str = "report_checks" ,
7879 benchmarks : str = "report_benchmarks" ,
80+ locks : str = "locks" ,
7981 ):
8082 super ().__init__ ()
8183 self .event_sender = event_sender
@@ -95,34 +97,28 @@ def __init__(
9597 self .report_check_db = report_check_db (self .db , report_checks )
9698 self .benchmark_db = benchmark_db (self .db , benchmarks )
9799 self .time_series_db = TimeSeriesDB (self .db , time_series , config )
100+ self .lock_db = LockDB (self .db , locks )
98101 self .graph_dbs : Dict [str , GraphDB ] = {}
99102 self .config = config
100103 self .cleaner = Periodic ("outdated_updates_cleaner" , self .check_outdated_updates , timedelta (seconds = 60 ))
101104
102105 async def start (self ) -> None :
103- await self .__migrate ()
104106 await self .cleaner .start ()
105107
106108 async def stop (self ) -> None :
107109 await self .cleaner .stop ()
108110
109- async def __migrate (self ) -> None :
110- try :
111- system_data = await self .system_data_db .system_data ()
112- except Exception :
113- system_data = None
114- if not await self .db .has_collection ("system_data" ): # make sure the system data collection exists
115- await self .db .create_collection ("system_data" )
116- if system_data is None : # in case no version is available, create a genesis version
117- system_data = SystemData (uuid_str (), utc (), CurrentDatabaseVersion )
118- git_hash = current_git_hash ()
119- if (
120- MigrateAlways
121- or system_data .db_version != CurrentDatabaseVersion
122- or system_data .version is None
123- or git_hash is None
124- or git_hash != system_data .version
125- ):
111+ async def migrate (self ) -> DatabaseChange :
112+ async def sys_data () -> Optional [SystemData ]:
113+ try :
114+ return await self .system_data_db .system_data ()
115+ except Exception :
116+ if not await self .db .has_collection ("system_data" ): # make sure the system data collection exists
117+ await self .db .create_collection ("system_data" )
118+ return None
119+
120+ async def do_migrate (system_data : SystemData ) -> SystemData :
121+ git_hash = current_git_hash ()
126122 # check if we need to run a migration
127123 if system_data .db_version < CurrentDatabaseVersion :
128124 log .info (f"Database migration required: db={ system_data .db_version } -> latest={ CurrentDatabaseVersion } " )
@@ -131,7 +127,7 @@ async def __migrate(self) -> None:
131127 for version in range (system_data .db_version , CurrentDatabaseVersion ):
132128 log .info (f"Running migration { version } -> { version + 1 } " )
133129 await migrations [version ]()
134- system_data . db_version = CurrentDatabaseVersion
130+ system_data = evolve ( system_data , db_version = CurrentDatabaseVersion )
135131 await self .system_data_db .update_system_data (system_data )
136132
137133 # will be executed on every git change
@@ -159,10 +155,22 @@ async def __migrate(self) -> None:
159155 await em .create_update_schema ()
160156
161157 # update the system data version to the current git hash
162- system_data .version = git_hash
163-
158+ system_data = evolve (system_data , version = git_hash )
164159 # update the system data version to not migrate the next time
165160 await self .system_data_db .update_system_data (system_data )
161+ return system_data
162+
163+ existing = await sys_data ()
164+ if existing is None or existing .detect_change ():
165+ # create `lock` collection to be able to create database locks
166+ await self .lock_db .create_update_schema ()
167+ async with self .lock_db .lock ("db_access.migrate" ):
168+ # lookup again with an exclusive database lock
169+ existing = await sys_data ()
170+ if existing is None or existing .detect_change ():
171+ updated = await do_migrate (existing or SystemData (uuid_str (), utc (), CurrentDatabaseVersion ))
172+ return DatabaseChange (existing , updated )
173+ return DatabaseChange (existing , existing )
166174
167175 async def __migrate_v1_to_v2 (self ) -> None :
168176 def migrate_config (old_id : str , old_root : str , config : Json ) -> Json :
0 commit comments