55from argparse import ArgumentParser , Namespace
66from multiprocessing import Process , Queue
77from pathlib import Path
8+ from random import randint
89from typing import Any , Generic , TypeVar
10+ from uuid import uuid4
911
1012from requests .exceptions import ConnectionError
1113from typing_extensions import assert_never
1214
13- from cognite .client .exceptions import CogniteAPIError , CogniteAuthError , CogniteConnectionError
15+ from cognite .client import CogniteClient
16+ from cognite .client .exceptions import (
17+ CogniteAPIError ,
18+ CogniteAuthError ,
19+ CogniteConnectionError ,
20+ )
1421from cognite .extractorutils .threading import CancellationToken
1522from cognite .extractorutils .unstable .configuration .exceptions import InvalidConfigError
16- from cognite .extractorutils .unstable .configuration .loaders import load_file , load_from_cdf
23+ from cognite .extractorutils .unstable .configuration .loaders import (
24+ load_file ,
25+ load_from_cdf ,
26+ )
1727from cognite .extractorutils .unstable .configuration .models import ConnectionConfig
1828from cognite .extractorutils .unstable .core ._dto import Error
29+ from cognite .extractorutils .unstable .core .errors import ErrorLevel
30+ from cognite .extractorutils .util import now
1931
2032from ._messaging import RuntimeMessage
2133from .base import ConfigRevision , ConfigType , Extractor , FullConfig
2638
2739
2840class Runtime (Generic [ExtractorType ]):
41+ RETRY_CONFIG_INTERVAL = 30
42+
2943 def __init__ (
3044 self ,
3145 extractor : type [ExtractorType ],
@@ -37,6 +51,8 @@ def __init__(
3751 self .logger = logging .getLogger (f"{ self ._extractor_class .EXTERNAL_ID } .runtime" )
3852 self ._setup_logging ()
3953
54+ self ._cognite_client : CogniteClient
55+
4056 def _create_argparser (self ) -> ArgumentParser :
4157 argparser = ArgumentParser (
4258 prog = sys .argv [0 ],
@@ -121,7 +137,7 @@ def _spawn_extractor(
121137 self .logger .info (f"Started extractor with PID { process .pid } " )
122138 return process
123139
124- def _get_application_config (
140+ def _try_get_application_config (
125141 self ,
126142 args : Namespace ,
127143 connection_config : ConnectionConfig ,
@@ -143,39 +159,65 @@ def _get_application_config(
143159
144160 else :
145161 self .logger .info ("Loading application config from CDF" )
146- client = connection_config .get_cognite_client (
147- f"{ self ._extractor_class .EXTERNAL_ID } -{ self ._extractor_class .VERSION } "
162+
163+ application_config , current_config_revision = load_from_cdf (
164+ self ._cognite_client ,
165+ connection_config .integration ,
166+ self ._extractor_class .CONFIG_TYPE ,
148167 )
149168
150- errors : list [Error ] = []
169+ return application_config , current_config_revision
170+
171+ def _safe_get_application_config (
172+ self ,
173+ args : Namespace ,
174+ connection_config : ConnectionConfig ,
175+ ) -> tuple [ConfigType , ConfigRevision ] | None :
176+ prev_error : str | None = None
151177
178+ while not self ._cancellation_token .is_cancelled :
152179 try :
153- application_config , current_config_revision = load_from_cdf (
154- client ,
155- connection_config .integration ,
156- self ._extractor_class .CONFIG_TYPE ,
180+ return self ._try_get_application_config (args , connection_config )
181+
182+ except Exception as e :
183+ error_message = str (e )
184+ if error_message == prev_error :
185+ # Same error as before, no need to log it again
186+ self ._cancellation_token .wait (randint (1 , self .RETRY_CONFIG_INTERVAL ))
187+ continue
188+ prev_error = error_message
189+
190+ ts = now ()
191+ error = Error (
192+ external_id = str (uuid4 ()),
193+ level = ErrorLevel .fatal .value ,
194+ start_time = ts ,
195+ end_time = ts ,
196+ description = error_message ,
197+ details = None ,
198+ task = None ,
157199 )
158200
159- finally :
160- if errors :
161- client .post (
162- f"/api/v1/projects/{ client .config .project } /odin/checkin" ,
163- json = {
164- "externalId" : connection_config .integration ,
165- "errors" : [e .model_dump () for e in errors ],
166- },
167- headers = {"cdf-version" : "alpha" },
168- )
201+ self ._cognite_client .post (
202+ f"/api/v1/projects/{ self ._cognite_client .config .project } /odin/checkin" ,
203+ json = {
204+ "externalId" : connection_config .integration ,
205+ "errors" : [error .model_dump ()],
206+ },
207+ headers = {"cdf-version" : "alpha" },
208+ )
169209
170- return application_config , current_config_revision
210+ self ._cancellation_token .wait (randint (1 , self .RETRY_CONFIG_INTERVAL ))
211+
212+ return None
171213
172214 def _verify_connection_config (self , connection_config : ConnectionConfig ) -> bool :
173- client = connection_config .get_cognite_client (
215+ self . _cognite_client = connection_config .get_cognite_client (
174216 f"{ self ._extractor_class .EXTERNAL_ID } -{ self ._extractor_class .VERSION } "
175217 )
176218 try :
177- client .post (
178- f"/api/v1/projects/{ client .config .project } /odin/checkin" ,
219+ self . _cognite_client .post (
220+ f"/api/v1/projects/{ self . _cognite_client .config .project } /odin/checkin" ,
179221 json = {
180222 "externalId" : connection_config .integration ,
181223 },
@@ -234,16 +276,19 @@ def run(self) -> None:
234276 if not args .skip_init_checks and not self ._verify_connection_config (connection_config ):
235277 sys .exit (1 )
236278
237- # This has to be Any. We don't know the type of the extractors' config at type checking since the sel doesn't
279+ # This has to be Any. We don't know the type of the extractors' config at type checking since the self doesn't
238280 # exist yet, and I have not found a way to represent it in a generic way that isn't just an Any in disguise.
239281 application_config : Any
282+ config : tuple [Any , ConfigRevision ] | None
283+
240284 while not self ._cancellation_token .is_cancelled :
241- try :
242- application_config , current_config_revision = self ._get_application_config (args , connection_config )
285+ config = self ._safe_get_application_config (args , connection_config )
286+ if config is None :
287+ if self ._cancellation_token .is_cancelled :
288+ break
289+ continue
243290
244- except InvalidConfigError :
245- self .logger .critical ("Could not get a valid application config file. Shutting down" )
246- sys .exit (1 )
291+ application_config , current_config_revision = config
247292
248293 # Start extractor in separate process, and wait for it to end
249294 process = self ._spawn_extractor (
0 commit comments