cognite-extractor-utils 7.6.0__py3-none-any.whl → 7.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cognite-extractor-utils might be problematic. Click here for more details.

Files changed (38) hide show
  1. cognite/examples/unstable/extractors/simple_extractor/config/config.yaml +3 -0
  2. cognite/examples/unstable/extractors/simple_extractor/config/connection_config.yaml +10 -0
  3. cognite/examples/unstable/extractors/simple_extractor/main.py +81 -0
  4. cognite/extractorutils/__init__.py +1 -1
  5. cognite/extractorutils/_inner_util.py +2 -2
  6. cognite/extractorutils/base.py +1 -1
  7. cognite/extractorutils/configtools/elements.py +4 -2
  8. cognite/extractorutils/configtools/loaders.py +3 -3
  9. cognite/extractorutils/exceptions.py +1 -1
  10. cognite/extractorutils/metrics.py +8 -6
  11. cognite/extractorutils/statestore/watermark.py +6 -3
  12. cognite/extractorutils/threading.py +2 -2
  13. cognite/extractorutils/unstable/configuration/exceptions.py +28 -1
  14. cognite/extractorutils/unstable/configuration/models.py +157 -32
  15. cognite/extractorutils/unstable/core/_dto.py +80 -7
  16. cognite/extractorutils/unstable/core/base.py +175 -106
  17. cognite/extractorutils/unstable/core/checkin_worker.py +428 -0
  18. cognite/extractorutils/unstable/core/errors.py +2 -2
  19. cognite/extractorutils/unstable/core/logger.py +49 -0
  20. cognite/extractorutils/unstable/core/runtime.py +200 -31
  21. cognite/extractorutils/unstable/core/tasks.py +2 -2
  22. cognite/extractorutils/uploader/__init__.py +2 -0
  23. cognite/extractorutils/uploader/_base.py +1 -1
  24. cognite/extractorutils/uploader/assets.py +1 -1
  25. cognite/extractorutils/uploader/data_modeling.py +1 -1
  26. cognite/extractorutils/uploader/events.py +1 -1
  27. cognite/extractorutils/uploader/files.py +4 -4
  28. cognite/extractorutils/uploader/raw.py +1 -1
  29. cognite/extractorutils/uploader/time_series.py +319 -52
  30. cognite/extractorutils/uploader_extractor.py +20 -5
  31. cognite/extractorutils/uploader_types.py +13 -2
  32. cognite/extractorutils/util.py +8 -6
  33. {cognite_extractor_utils-7.6.0.dist-info → cognite_extractor_utils-7.8.0.dist-info}/METADATA +3 -2
  34. cognite_extractor_utils-7.8.0.dist-info/RECORD +55 -0
  35. cognite_extractor_utils-7.8.0.dist-info/entry_points.txt +2 -0
  36. cognite_extractor_utils-7.6.0.dist-info/RECORD +0 -50
  37. {cognite_extractor_utils-7.6.0.dist-info → cognite_extractor_utils-7.8.0.dist-info}/WHEEL +0 -0
  38. {cognite_extractor_utils-7.6.0.dist-info → cognite_extractor_utils-7.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,10 +2,16 @@
2
2
  Temporary holding place for DTOs against Extraction Pipelines 2.0 until it's in the SDK.
3
3
  """
4
4
 
5
- from typing import Any, Literal
5
+ from enum import Enum
6
+ from typing import Annotated, Any, Literal, Optional
6
7
 
8
+ from annotated_types import Len
7
9
  from humps import camelize
8
- from pydantic import BaseModel, ConfigDict
10
+ from pydantic import BaseModel, ConfigDict, StringConstraints
11
+ from typing_extensions import TypeAliasType
12
+
13
+ from cognite.extractorutils.unstable.core.errors import Error as InternalError
14
+ from cognite.extractorutils.unstable.core.errors import ErrorLevel
9
15
 
10
16
 
11
17
  class CogniteModel(BaseModel):
@@ -17,30 +23,97 @@ class CogniteModel(BaseModel):
17
23
  * exclude Nones from serialized JSON instead of having nulls in the response text.
18
24
  """
19
25
 
20
- def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
26
+ def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]: # noqa: ANN401
21
27
  if kwargs:
22
28
  kwargs["exclude_none"] = True
23
29
  else:
24
30
  kwargs = {"exclude_none": True}
25
31
  return BaseModel.model_dump(self, *args, **kwargs)
26
32
 
27
- def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
33
+ def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: # noqa: ANN401
28
34
  return self.model_dump(*args, **kwargs)
29
35
 
30
36
  model_config = ConfigDict(alias_generator=camelize, populate_by_name=True, extra="forbid")
31
37
 
32
38
 
39
+ class WithExternalId(CogniteModel):
40
+ external_id: str
41
+
42
+
43
+ MessageType = Annotated[str, StringConstraints(min_length=0, max_length=1000)]
44
+
45
+
33
46
  class TaskUpdate(CogniteModel):
34
47
  type: Literal["started"] | Literal["ended"]
35
48
  name: str
36
49
  timestamp: int
50
+ message: MessageType | None = None
37
51
 
38
52
 
39
- class Error(CogniteModel):
40
- external_id: str
41
- level: str
53
+ class Error(WithExternalId):
54
+ level: ErrorLevel
42
55
  description: str
43
56
  details: str | None
44
57
  start_time: int
45
58
  end_time: int | None
46
59
  task: str | None
60
+
61
+ @classmethod
62
+ def from_internal(cls, error: InternalError) -> "Error":
63
+ """Convert the error into a DTO (Data Transfer Object) for reporting."""
64
+ return Error(
65
+ external_id=error.external_id,
66
+ level=error.level,
67
+ description=error.description,
68
+ details=error.details,
69
+ start_time=error.start_time,
70
+ end_time=error.end_time,
71
+ task=error._task_name,
72
+ )
73
+
74
+
75
+ TaskUpdateList = Annotated[list[TaskUpdate], Len(min_length=1, max_length=1000)]
76
+ ErrorList = Annotated[list[Error], Len(min_length=0, max_length=1000)]
77
+ VersionType = Annotated[str, StringConstraints(min_length=1, max_length=32)]
78
+ DescriptionType = Annotated[str, StringConstraints(min_length=0, max_length=500)]
79
+ TaskList = Annotated[list["Task"], Len(min_length=1, max_length=1000)]
80
+ JSONType = TypeAliasType( # type: ignore[misc]
81
+ "JSONType",
82
+ bool | int | float | str | None | list[Optional["JSONType"]] | dict[str, Optional["JSONType"]], # type: ignore[misc]
83
+ )
84
+
85
+
86
+ class WithVersion(CogniteModel):
87
+ version: VersionType | None = None
88
+
89
+
90
+ class ExtractorInfo(WithExternalId, WithVersion):
91
+ pass
92
+
93
+
94
+ class TaskType(Enum):
95
+ continuous = "continuous"
96
+ batch = "batch"
97
+
98
+
99
+ class Task(CogniteModel):
100
+ type: TaskType
101
+ name: str
102
+ action: bool = False
103
+ description: DescriptionType | None = None
104
+
105
+
106
+ class StartupRequest(WithExternalId):
107
+ extractor: ExtractorInfo
108
+ tasks: TaskList | None = None
109
+ active_config_revision: int | Literal["local"] | None = None
110
+ timestamp: int | None = None
111
+
112
+
113
+ class CheckinRequest(WithExternalId):
114
+ task_events: TaskUpdateList | None = None
115
+ errors: ErrorList | None = None
116
+
117
+
118
+ class CheckinResponse(WithExternalId):
119
+ last_config_revision: int | None = None
@@ -47,38 +47,56 @@ The subclass should also define several class attributes:
47
47
  import logging
48
48
  import time
49
49
  from concurrent.futures import ThreadPoolExecutor
50
+ from datetime import datetime, timezone
50
51
  from functools import partial
51
- from logging.handlers import TimedRotatingFileHandler
52
52
  from multiprocessing import Queue
53
+ from multiprocessing.synchronize import Event as MpEvent
53
54
  from threading import RLock, Thread
54
55
  from types import TracebackType
55
- from typing import Generic, Literal, TypeVar
56
+ from typing import Generic, TypeVar
56
57
 
57
58
  from humps import pascalize
58
59
  from typing_extensions import Self, assert_never
59
60
 
60
61
  from cognite.extractorutils._inner_util import _resolve_log_level
62
+ from cognite.extractorutils.statestore import (
63
+ AbstractStateStore,
64
+ LocalStateStore,
65
+ NoStateStore,
66
+ )
61
67
  from cognite.extractorutils.threading import CancellationToken
62
68
  from cognite.extractorutils.unstable.configuration.models import (
69
+ ConfigRevision,
70
+ ConfigType,
63
71
  ConnectionConfig,
64
72
  ExtractorConfig,
65
73
  LogConsoleHandlerConfig,
66
74
  LogFileHandlerConfig,
67
75
  )
68
- from cognite.extractorutils.unstable.core._dto import Error as DtoError
69
- from cognite.extractorutils.unstable.core._dto import TaskUpdate
76
+ from cognite.extractorutils.unstable.core._dto import (
77
+ CogniteModel,
78
+ ExtractorInfo,
79
+ StartupRequest,
80
+ TaskType,
81
+ )
82
+ from cognite.extractorutils.unstable.core._dto import (
83
+ Task as DtoTask,
84
+ )
70
85
  from cognite.extractorutils.unstable.core._messaging import RuntimeMessage
86
+ from cognite.extractorutils.unstable.core.checkin_worker import CheckinWorker
71
87
  from cognite.extractorutils.unstable.core.errors import Error, ErrorLevel
72
- from cognite.extractorutils.unstable.core.logger import CogniteLogger
88
+ from cognite.extractorutils.unstable.core.logger import CogniteLogger, RobustFileHandler
73
89
  from cognite.extractorutils.unstable.core.restart_policy import WHEN_CONTINUOUS_TASKS_CRASHES, RestartPolicy
74
90
  from cognite.extractorutils.unstable.core.tasks import ContinuousTask, ScheduledTask, StartupTask, Task, TaskContext
75
91
  from cognite.extractorutils.unstable.scheduling import TaskScheduler
76
92
  from cognite.extractorutils.util import now
77
93
 
78
- __all__ = ["ConfigRevision", "ConfigType", "Extractor"]
79
-
80
- ConfigType = TypeVar("ConfigType", bound=ExtractorConfig)
81
- ConfigRevision = Literal["local"] | int
94
+ __all__ = [
95
+ "CogniteModel",
96
+ "ConfigRevision",
97
+ "ConfigType",
98
+ "Extractor",
99
+ ]
82
100
 
83
101
 
84
102
  _T = TypeVar("_T", bound=ExtractorConfig)
@@ -97,10 +115,12 @@ class FullConfig(Generic[_T]):
97
115
  connection_config: ConnectionConfig,
98
116
  application_config: _T,
99
117
  current_config_revision: ConfigRevision,
118
+ log_level_override: str | None = None,
100
119
  ) -> None:
101
120
  self.connection_config = connection_config
102
121
  self.application_config = application_config
103
- self.current_config_revision = current_config_revision
122
+ self.current_config_revision: ConfigRevision = current_config_revision
123
+ self.log_level_override = log_level_override
104
124
 
105
125
 
106
126
  class Extractor(Generic[ConfigType], CogniteLogger):
@@ -122,33 +142,60 @@ class Extractor(Generic[ConfigType], CogniteLogger):
122
142
  CONFIG_TYPE: type[ConfigType]
123
143
 
124
144
  RESTART_POLICY: RestartPolicy = WHEN_CONTINUOUS_TASKS_CRASHES
145
+ USE_DEFAULT_STATE_STORE: bool = True
146
+ _statestore_singleton: AbstractStateStore | None = None
147
+
148
+ cancellation_token: CancellationToken
125
149
 
126
- def __init__(self, config: FullConfig[ConfigType]) -> None:
150
+ def __init__(self, config: FullConfig[ConfigType], checkin_worker: CheckinWorker) -> None:
127
151
  self._logger = logging.getLogger(f"{self.EXTERNAL_ID}.main")
152
+ self._checkin_worker = checkin_worker
128
153
 
129
154
  self.cancellation_token = CancellationToken()
130
155
  self.cancellation_token.cancel_on_interrupt()
131
156
 
132
157
  self.connection_config = config.connection_config
133
158
  self.application_config = config.application_config
134
- self.current_config_revision = config.current_config_revision
159
+ self.current_config_revision: ConfigRevision = config.current_config_revision
160
+ self.log_level_override = config.log_level_override
135
161
 
136
162
  self.cognite_client = self.connection_config.get_cognite_client(f"{self.EXTERNAL_ID}-{self.VERSION}")
137
163
 
164
+ self.state_store: AbstractStateStore
165
+
138
166
  self._checkin_lock = RLock()
139
167
  self._runtime_messages: Queue[RuntimeMessage] | None = None
140
168
 
141
169
  self._scheduler = TaskScheduler(self.cancellation_token.create_child_token())
142
170
 
143
171
  self._tasks: list[Task] = []
144
- self._task_updates: list[TaskUpdate] = []
145
- self._errors: dict[str, Error] = {}
172
+ self._start_time: datetime
146
173
 
147
174
  self.__init_tasks__()
148
175
 
176
+ def _setup_cancellation_watcher(self, cancel_event: MpEvent) -> None:
177
+ """Starts a daemon thread to watch the inter-process event."""
178
+
179
+ def watcher() -> None:
180
+ """Blocks until the event is set, then cancels the local token."""
181
+ cancel_event.wait()
182
+ if not self.cancellation_token.is_cancelled:
183
+ self._logger.info("Cancellation signal received from runtime. Shutting down gracefully.")
184
+ self.cancellation_token.cancel()
185
+
186
+ thread = Thread(target=watcher, name="RuntimeCancellationWatcher", daemon=True)
187
+ thread.start()
188
+
149
189
  def _setup_logging(self) -> None:
150
- min_level = min([_resolve_log_level(h.level.value) for h in self.application_config.log_handlers])
151
- max_level = max([_resolve_log_level(h.level.value) for h in self.application_config.log_handlers])
190
+ if self.log_level_override:
191
+ level_to_set = _resolve_log_level(self.log_level_override)
192
+ # Use the override level if provided
193
+ min_level = level_to_set
194
+ max_level = level_to_set
195
+ else:
196
+ # Otherwise, use the levels from the config file
197
+ min_level = min([_resolve_log_level(h.level.value) for h in self.application_config.log_handlers])
198
+ max_level = max([_resolve_log_level(h.level.value) for h in self.application_config.log_handlers])
152
199
 
153
200
  root = logging.getLogger()
154
201
  root.setLevel(min_level)
@@ -173,21 +220,85 @@ class Extractor(Generic[ConfigType], CogniteLogger):
173
220
  case LogConsoleHandlerConfig() as console_handler:
174
221
  sh = logging.StreamHandler()
175
222
  sh.setFormatter(fmt)
176
- sh.setLevel(_resolve_log_level(console_handler.level.value))
223
+ level_for_handler = _resolve_log_level(
224
+ self.log_level_override if self.log_level_override else console_handler.level.value
225
+ )
226
+ sh.setLevel(level_for_handler)
177
227
 
178
228
  root.addHandler(sh)
179
229
 
180
230
  case LogFileHandlerConfig() as file_handler:
181
- fh = TimedRotatingFileHandler(
182
- filename=file_handler.path,
183
- when="midnight",
184
- utc=True,
185
- backupCount=file_handler.retention,
186
- )
187
- fh.setLevel(_resolve_log_level(file_handler.level.value))
188
- fh.setFormatter(fmt)
231
+ try:
232
+ fh = RobustFileHandler(
233
+ filename=file_handler.path,
234
+ when="midnight",
235
+ utc=True,
236
+ backupCount=file_handler.retention,
237
+ create_dirs=True,
238
+ )
239
+ level_for_handler = _resolve_log_level(
240
+ self.log_level_override if self.log_level_override else file_handler.level.value
241
+ )
242
+ fh.setLevel(level_for_handler)
243
+ fh.setFormatter(fmt)
244
+
245
+ root.addHandler(fh)
246
+ except (OSError, PermissionError) as e:
247
+ self._logger.warning(
248
+ f"Could not create or write to log file {file_handler.path}: {e}. "
249
+ "Falling back to console logging."
250
+ )
251
+ if not any(isinstance(h, logging.StreamHandler) for h in root.handlers):
252
+ sh = logging.StreamHandler()
253
+ sh.setFormatter(fmt)
254
+ sh.setLevel(level_for_handler)
255
+ root.addHandler(sh)
256
+
257
+ def _load_state_store(self) -> None:
258
+ """
259
+ Searches through the config object for a StateStoreConfig.
260
+
261
+ If found, it will use that configuration to generate a state store, if no such config is found it will either
262
+ create a LocalStateStore or a NoStateStore depending on whether the ``use_default_state_store`` argument to the
263
+ constructor was true or false.
264
+
265
+ Either way, the state_store attribute is guaranteed to be set after calling this method.
266
+ """
267
+ state_store_config = self.application_config.state_store
268
+
269
+ if state_store_config:
270
+ self.state_store = state_store_config.create_state_store(
271
+ cdf_client=self.cognite_client,
272
+ default_to_local=self.USE_DEFAULT_STATE_STORE,
273
+ cancellation_token=self.cancellation_token,
274
+ )
275
+ elif self.USE_DEFAULT_STATE_STORE:
276
+ self.state_store = LocalStateStore("states.json", cancellation_token=self.cancellation_token)
277
+ else:
278
+ self.state_store = NoStateStore()
279
+
280
+ try:
281
+ self.state_store.initialize()
282
+ except ValueError:
283
+ self._logger.exception("Could not load state store, using an empty state store as default")
284
+ self.state_store = NoStateStore()
189
285
 
190
- root.addHandler(fh)
286
+ Extractor._statestore_singleton = self.state_store
287
+
288
+ @classmethod
289
+ def get_current_statestore(cls) -> AbstractStateStore:
290
+ """
291
+ Get the current state store singleton.
292
+
293
+ Returns:
294
+ The current state store singleton
295
+
296
+ Raises:
297
+ ValueError: If no state store singleton has been created, meaning no state store has been loaded.
298
+ """
299
+ if Extractor._statestore_singleton is None:
300
+ raise ValueError("No state store singleton created. Have a state store been loaded?")
301
+ return Extractor._statestore_singleton
191
302
 
192
303
  def __init_tasks__(self) -> None:
193
304
  """
@@ -202,55 +313,37 @@ class Extractor(Generic[ConfigType], CogniteLogger):
202
313
  def _set_runtime_message_queue(self, queue: Queue) -> None:
203
314
  self._runtime_messages = queue
204
315
 
205
- def _checkin(self) -> None:
206
- with self._checkin_lock:
207
- task_updates = [t.model_dump() for t in self._task_updates]
208
- self._task_updates.clear()
209
-
210
- error_updates = [
211
- DtoError(
212
- external_id=e.external_id,
213
- level=e.level.value,
214
- description=e.description,
215
- details=e.details,
216
- start_time=e.start_time,
217
- end_time=e.end_time,
218
- task=e._task_name if e._task_name is not None else None,
219
- ).model_dump()
220
- for e in self._errors.values()
316
+ def _attach_runtime_controls(self, *, cancel_event: MpEvent, message_queue: Queue) -> None:
317
+ self._set_runtime_message_queue(message_queue)
318
+ self._setup_cancellation_watcher(cancel_event)
319
+
320
+ def _get_startup_request(self) -> StartupRequest:
321
+ return StartupRequest(
322
+ external_id=self.connection_config.integration.external_id,
323
+ active_config_revision=self.current_config_revision,
324
+ extractor=ExtractorInfo(version=self.VERSION, external_id=self.EXTERNAL_ID),
325
+ tasks=[
326
+ DtoTask(
327
+ type=TaskType.continuous if isinstance(t, ContinuousTask) else TaskType.batch,
328
+ action=isinstance(t, ScheduledTask),
329
+ description=t.description,
330
+ name=t.name,
331
+ )
332
+ for t in self._tasks
221
333
  ]
222
- self._errors.clear()
223
-
224
- res = self.cognite_client.post(
225
- f"/api/v1/projects/{self.cognite_client.config.project}/integrations/checkin",
226
- json={
227
- "externalId": self.connection_config.integration,
228
- "taskEvents": task_updates,
229
- "errors": error_updates,
230
- },
231
- headers={"cdf-version": "alpha"},
334
+ if len(self._tasks) > 0
335
+ else None,
336
+ timestamp=int(self._start_time.timestamp() * 1000),
232
337
  )
233
- new_config_revision = res.json().get("lastConfigRevision")
234
-
235
- if (
236
- new_config_revision
237
- and self.current_config_revision != "local"
238
- and new_config_revision > self.current_config_revision
239
- ):
240
- self.restart()
241
338
 
242
339
  def _run_checkin(self) -> None:
243
- while not self.cancellation_token.is_cancelled:
244
- try:
245
- self._logger.debug("Running checkin")
246
- self._checkin()
247
- except Exception:
248
- self._logger.exception("Error during checkin")
249
- self.cancellation_token.wait(10)
340
+ self._checkin_worker.run_periodic_checkin(self.cancellation_token, self._get_startup_request())
250
341
 
251
342
  def _report_error(self, error: Error) -> None:
252
- with self._checkin_lock:
253
- self._errors[error.external_id] = error
343
+ self._checkin_worker.report_error(error)
344
+
345
+ def _try_report_error(self, error: Error) -> None:
346
+ self._checkin_worker.try_report_error(error)
254
347
 
255
348
  def _new_error(
256
349
  self,
@@ -278,8 +371,8 @@ class Extractor(Generic[ConfigType], CogniteLogger):
278
371
  self.cancellation_token.cancel()
279
372
 
280
373
  @classmethod
281
- def _init_from_runtime(cls, config: FullConfig[ConfigType]) -> Self:
282
- return cls(config)
374
+ def _init_from_runtime(cls, config: FullConfig[ConfigType], checkin_worker: CheckinWorker) -> Self:
375
+ return cls(config, checkin_worker)
283
376
 
284
377
  def add_task(self, task: Task) -> None:
285
378
  """
@@ -298,10 +391,7 @@ class Extractor(Generic[ConfigType], CogniteLogger):
298
391
  A wrapped version of the task's target, with tracking and error handling.
299
392
  """
300
393
  # Record a task start
301
- with self._checkin_lock:
302
- self._task_updates.append(
303
- TaskUpdate(type="started", name=task.name, timestamp=now()),
304
- )
394
+ self._checkin_worker.report_task_start(name=task.name, timestamp=now())
305
395
 
306
396
  try:
307
397
  # Run task
@@ -320,10 +410,7 @@ class Extractor(Generic[ConfigType], CogniteLogger):
320
410
 
321
411
  finally:
322
412
  # Record task end
323
- with self._checkin_lock:
324
- self._task_updates.append(
325
- TaskUpdate(type="ended", name=task.name, timestamp=now()),
326
- )
413
+ self._checkin_worker.report_task_end(name=task.name, timestamp=now())
327
414
 
328
415
  task.target = run_task
329
416
  self._tasks.append(task)
@@ -341,29 +428,6 @@ class Extractor(Generic[ConfigType], CogniteLogger):
341
428
  ),
342
429
  )
343
430
 
344
- def _report_extractor_info(self) -> None:
345
- self.cognite_client.post(
346
- f"/api/v1/projects/{self.cognite_client.config.project}/integrations/extractorinfo",
347
- json={
348
- "externalId": self.connection_config.integration,
349
- "activeConfigRevision": self.current_config_revision,
350
- "extractor": {
351
- "version": self.VERSION,
352
- "externalId": self.EXTERNAL_ID,
353
- },
354
- "tasks": [
355
- {
356
- "name": t.name,
357
- "type": "continuous" if isinstance(t, ContinuousTask) else "batch",
358
- "action": bool(isinstance(t, ScheduledTask)),
359
- "description": t.description,
360
- }
361
- for t in self._tasks
362
- ],
363
- },
364
- headers={"cdf-version": "alpha"},
365
- )
366
-
367
431
  def start(self) -> None:
368
432
  """
369
433
  Start the extractor.
@@ -372,7 +436,11 @@ class Extractor(Generic[ConfigType], CogniteLogger):
372
436
  ``with`` statement, which ensures proper cleanup on exit.
373
437
  """
374
438
  self._setup_logging()
375
- self._report_extractor_info()
439
+ self._start_time = datetime.now(tz=timezone.utc)
440
+
441
+ self._load_state_store()
442
+ self.state_store.start()
443
+
376
444
  Thread(target=self._run_checkin, name="ExtractorCheckin", daemon=True).start()
377
445
 
378
446
  def stop(self) -> None:
@@ -401,10 +469,11 @@ class Extractor(Generic[ConfigType], CogniteLogger):
401
469
  Stop the extractor when exiting the context manager.
402
470
  """
403
471
  self.stop()
404
- with self._checkin_lock:
405
- self._checkin()
406
472
 
407
- self._logger.info("Shutting down extractor")
473
+ if self.state_store:
474
+ self.state_store.synchronize()
475
+
476
+ self._checkin_worker.flush(self.cancellation_token)
408
477
  return exc_val is None
409
478
 
410
479
  def run(self) -> None: