apache-airflow-providers-edge3 1.3.1__py3-none-any.whl → 1.4.0rc1__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 apache-airflow-providers-edge3 might be problematic. Click here for more details.

@@ -29,7 +29,7 @@ from airflow import __version__ as airflow_version
29
29
 
30
30
  __all__ = ["__version__"]
31
31
 
32
- __version__ = "1.3.1"
32
+ __version__ = "1.4.0"
33
33
 
34
34
  if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
35
35
  "2.10.0"
@@ -34,6 +34,7 @@ from requests import HTTPError
34
34
 
35
35
  from airflow import __version__ as airflow_version
36
36
  from airflow.configuration import conf
37
+ from airflow.providers.common.compat.sdk import timezone
37
38
  from airflow.providers.edge3 import __version__ as edge_provider_version
38
39
  from airflow.providers.edge3.cli.api_client import (
39
40
  jobs_fetch,
@@ -52,7 +53,6 @@ from airflow.providers.edge3.cli.signalling import (
52
53
  )
53
54
  from airflow.providers.edge3.models.edge_worker import EdgeWorkerState, EdgeWorkerVersionException
54
55
  from airflow.providers.edge3.version_compat import AIRFLOW_V_3_0_PLUS
55
- from airflow.utils import timezone
56
56
  from airflow.utils.net import getfqdn
57
57
  from airflow.utils.state import TaskInstanceState
58
58
 
@@ -67,7 +67,7 @@ if TYPE_CHECKING:
67
67
  try:
68
68
  from airflow.operators.python import PythonOperator
69
69
  except ImportError:
70
- from airflow.providers.common.compat.standard.operators import PythonOperator
70
+ from airflow.providers.common.compat.standard.operators import PythonOperator # type: ignore[no-redef]
71
71
 
72
72
 
73
73
  class CmdOperator(BaseOperator):
@@ -31,13 +31,13 @@ from airflow.cli.cli_config import GroupCommand
31
31
  from airflow.configuration import conf
32
32
  from airflow.executors.base_executor import BaseExecutor
33
33
  from airflow.models.taskinstance import TaskInstance, TaskInstanceState
34
+ from airflow.providers.common.compat.sdk import timezone
34
35
  from airflow.providers.edge3.cli.edge_command import EDGE_COMMANDS
35
36
  from airflow.providers.edge3.models.edge_job import EdgeJobModel
36
37
  from airflow.providers.edge3.models.edge_logs import EdgeLogsModel
37
38
  from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel, EdgeWorkerState, reset_metrics
38
39
  from airflow.providers.edge3.version_compat import AIRFLOW_V_3_0_PLUS
39
40
  from airflow.stats import Stats
40
- from airflow.utils import timezone
41
41
  from airflow.utils.db import DBLocks, create_global_lock
42
42
  from airflow.utils.session import NEW_SESSION, provide_session
43
43
 
@@ -352,7 +352,7 @@ class EdgeExecutor(BaseExecutor):
352
352
  del self.last_reported_state[job.key]
353
353
  self.fail(job.key)
354
354
  else:
355
- self.last_reported_state[job.key] = job.state
355
+ self.last_reported_state[job.key] = TaskInstanceState(job.state)
356
356
  if (
357
357
  job.state == TaskInstanceState.SUCCESS
358
358
  and job.last_update_t < (datetime.now() - timedelta(minutes=job_success_purge)).timestamp()
@@ -399,6 +399,35 @@ class EdgeExecutor(BaseExecutor):
399
399
  def terminate(self):
400
400
  """Terminate the executor is not doing anything."""
401
401
 
402
+ @provide_session
403
+ def revoke_task(self, *, ti: TaskInstance, session: Session = NEW_SESSION):
404
+ """
405
+ Revoke a task instance from the executor.
406
+
407
+ This method removes the task from the executor's internal state and deletes
408
+ the corresponding EdgeJobModel record to prevent edge workers from picking it up.
409
+
410
+ :param ti: Task instance to revoke
411
+ :param session: Database session
412
+ """
413
+ # Remove from executor's internal state
414
+ self.running.discard(ti.key)
415
+ self.queued_tasks.pop(ti.key, None)
416
+ if ti.key in self.last_reported_state:
417
+ del self.last_reported_state[ti.key]
418
+
419
+ # Delete the job from the database to prevent edge workers from picking it up
420
+ session.execute(
421
+ delete(EdgeJobModel).where(
422
+ EdgeJobModel.dag_id == ti.dag_id,
423
+ EdgeJobModel.task_id == ti.task_id,
424
+ EdgeJobModel.run_id == ti.run_id,
425
+ EdgeJobModel.map_index == ti.map_index,
426
+ EdgeJobModel.try_number == ti.try_number,
427
+ )
428
+ )
429
+ self.log.info("Revoked task instance %s from EdgeExecutor", ti.key)
430
+
402
431
  def try_adopt_task_instances(self, tis: Sequence[TaskInstance]) -> Sequence[TaskInstance]:
403
432
  """
404
433
  Try to adopt running task instances that have been abandoned by a SchedulerJob dying.
@@ -19,16 +19,17 @@ from __future__ import annotations
19
19
  from datetime import datetime
20
20
 
21
21
  from sqlalchemy import (
22
- Column,
23
22
  Index,
24
23
  Integer,
25
24
  String,
26
25
  text,
27
26
  )
27
+ from sqlalchemy.orm import Mapped
28
28
 
29
29
  from airflow.models.base import Base, StringID
30
30
  from airflow.models.taskinstancekey import TaskInstanceKey
31
- from airflow.utils import timezone
31
+ from airflow.providers.common.compat.sdk import timezone
32
+ from airflow.providers.common.compat.sqlalchemy.orm import mapped_column
32
33
  from airflow.utils.log.logging_mixin import LoggingMixin
33
34
  from airflow.utils.sqlalchemy import UtcDateTime
34
35
 
@@ -41,18 +42,20 @@ class EdgeJobModel(Base, LoggingMixin):
41
42
  """
42
43
 
43
44
  __tablename__ = "edge_job"
44
- dag_id = Column(StringID(), primary_key=True, nullable=False)
45
- task_id = Column(StringID(), primary_key=True, nullable=False)
46
- run_id = Column(StringID(), primary_key=True, nullable=False)
47
- map_index = Column(Integer, primary_key=True, nullable=False, server_default=text("-1"))
48
- try_number = Column(Integer, primary_key=True, default=0)
49
- state = Column(String(20))
50
- queue = Column(String(256))
51
- concurrency_slots = Column(Integer)
52
- command = Column(String(2048))
53
- queued_dttm = Column(UtcDateTime)
54
- edge_worker = Column(String(64))
55
- last_update = Column(UtcDateTime)
45
+ dag_id: Mapped[str] = mapped_column(StringID(), primary_key=True, nullable=False)
46
+ task_id: Mapped[str] = mapped_column(StringID(), primary_key=True, nullable=False)
47
+ run_id: Mapped[str] = mapped_column(StringID(), primary_key=True, nullable=False)
48
+ map_index: Mapped[int] = mapped_column(
49
+ Integer, primary_key=True, nullable=False, server_default=text("-1")
50
+ )
51
+ try_number: Mapped[int] = mapped_column(Integer, primary_key=True, default=0)
52
+ state: Mapped[str] = mapped_column(String(20))
53
+ queue: Mapped[str] = mapped_column(String(256))
54
+ concurrency_slots: Mapped[int] = mapped_column(Integer)
55
+ command: Mapped[str] = mapped_column(String(2048))
56
+ queued_dttm: Mapped[datetime | None] = mapped_column(UtcDateTime)
57
+ edge_worker: Mapped[str | None] = mapped_column(String(64))
58
+ last_update: Mapped[datetime | None] = mapped_column(UtcDateTime)
56
59
 
57
60
  def __init__(
58
61
  self,
@@ -19,14 +19,15 @@ from __future__ import annotations
19
19
  from datetime import datetime
20
20
 
21
21
  from sqlalchemy import (
22
- Column,
23
22
  Integer,
24
23
  Text,
25
24
  text,
26
25
  )
27
26
  from sqlalchemy.dialects.mysql import MEDIUMTEXT
27
+ from sqlalchemy.orm import Mapped
28
28
 
29
29
  from airflow.models.base import Base, StringID
30
+ from airflow.providers.common.compat.sqlalchemy.orm import mapped_column
30
31
  from airflow.utils.log.logging_mixin import LoggingMixin
31
32
  from airflow.utils.sqlalchemy import UtcDateTime
32
33
 
@@ -45,13 +46,15 @@ class EdgeLogsModel(Base, LoggingMixin):
45
46
  """
46
47
 
47
48
  __tablename__ = "edge_logs"
48
- dag_id = Column(StringID(), primary_key=True, nullable=False)
49
- task_id = Column(StringID(), primary_key=True, nullable=False)
50
- run_id = Column(StringID(), primary_key=True, nullable=False)
51
- map_index = Column(Integer, primary_key=True, nullable=False, server_default=text("-1"))
52
- try_number = Column(Integer, primary_key=True, default=0)
53
- log_chunk_time = Column(UtcDateTime, primary_key=True, nullable=False)
54
- log_chunk_data = Column(Text().with_variant(MEDIUMTEXT(), "mysql"), nullable=False)
49
+ dag_id: Mapped[str] = mapped_column(StringID(), primary_key=True, nullable=False)
50
+ task_id: Mapped[str] = mapped_column(StringID(), primary_key=True, nullable=False)
51
+ run_id: Mapped[str] = mapped_column(StringID(), primary_key=True, nullable=False)
52
+ map_index: Mapped[int] = mapped_column(
53
+ Integer, primary_key=True, nullable=False, server_default=text("-1")
54
+ )
55
+ try_number: Mapped[int] = mapped_column(Integer, primary_key=True, default=0)
56
+ log_chunk_time: Mapped[datetime] = mapped_column(UtcDateTime, primary_key=True, nullable=False)
57
+ log_chunk_data: Mapped[str] = mapped_column(Text().with_variant(MEDIUMTEXT(), "mysql"), nullable=False)
55
58
 
56
59
  def __init__(
57
60
  self,
@@ -23,18 +23,22 @@ from datetime import datetime
23
23
  from enum import Enum
24
24
  from typing import TYPE_CHECKING
25
25
 
26
- from sqlalchemy import Column, Integer, String, delete, select
26
+ from sqlalchemy import Integer, String, delete, select
27
+ from sqlalchemy.orm import Mapped
27
28
 
28
29
  from airflow.exceptions import AirflowException
29
30
  from airflow.models.base import Base
31
+ from airflow.providers.common.compat.sdk import timezone
32
+ from airflow.providers.common.compat.sqlalchemy.orm import mapped_column
30
33
  from airflow.stats import Stats
31
- from airflow.utils import timezone
32
34
  from airflow.utils.log.logging_mixin import LoggingMixin
33
35
  from airflow.utils.providers_configuration_loader import providers_configuration_loaded
34
36
  from airflow.utils.session import NEW_SESSION, provide_session
35
37
  from airflow.utils.sqlalchemy import UtcDateTime
36
38
 
37
39
  if TYPE_CHECKING:
40
+ from collections.abc import Sequence
41
+
38
42
  from sqlalchemy.orm import Session
39
43
 
40
44
  logger = logging.getLogger(__name__)
@@ -79,29 +83,29 @@ class EdgeWorkerModel(Base, LoggingMixin):
79
83
  """A Edge Worker instance which reports the state and health."""
80
84
 
81
85
  __tablename__ = "edge_worker"
82
- worker_name = Column(String(64), primary_key=True, nullable=False)
83
- state = Column(String(20))
84
- maintenance_comment = Column(String(1024))
85
- _queues = Column("queues", String(256))
86
- first_online = Column(UtcDateTime)
87
- last_update = Column(UtcDateTime)
88
- jobs_active = Column(Integer, default=0)
89
- jobs_taken = Column(Integer, default=0)
90
- jobs_success = Column(Integer, default=0)
91
- jobs_failed = Column(Integer, default=0)
92
- sysinfo = Column(String(256))
86
+ worker_name: Mapped[str] = mapped_column(String(64), primary_key=True, nullable=False)
87
+ state: Mapped[EdgeWorkerState] = mapped_column(String(20))
88
+ maintenance_comment: Mapped[str | None] = mapped_column(String(1024))
89
+ _queues: Mapped[str | None] = mapped_column("queues", String(256))
90
+ first_online: Mapped[datetime | None] = mapped_column(UtcDateTime)
91
+ last_update: Mapped[datetime | None] = mapped_column(UtcDateTime)
92
+ jobs_active: Mapped[int] = mapped_column(Integer, default=0)
93
+ jobs_taken: Mapped[int] = mapped_column(Integer, default=0)
94
+ jobs_success: Mapped[int] = mapped_column(Integer, default=0)
95
+ jobs_failed: Mapped[int] = mapped_column(Integer, default=0)
96
+ sysinfo: Mapped[str | None] = mapped_column(String(256))
93
97
 
94
98
  def __init__(
95
99
  self,
96
100
  worker_name: str,
97
- state: str,
101
+ state: str | EdgeWorkerState,
98
102
  queues: list[str] | None,
99
103
  first_online: datetime | None = None,
100
104
  last_update: datetime | None = None,
101
105
  maintenance_comment: str | None = None,
102
106
  ):
103
107
  self.worker_name = worker_name
104
- self.state = state
108
+ self.state = EdgeWorkerState(state)
105
109
  self.queues = queues
106
110
  self.first_online = first_online or timezone.utcnow()
107
111
  self.last_update = last_update
@@ -139,14 +143,14 @@ class EdgeWorkerModel(Base, LoggingMixin):
139
143
  queues.remove(queue_name)
140
144
  self.queues = queues
141
145
 
142
- def update_state(self, state: str) -> None:
146
+ def update_state(self, state: str | EdgeWorkerState) -> None:
143
147
  """Update state field."""
144
- self.state = state
148
+ self.state = EdgeWorkerState(state)
145
149
 
146
150
 
147
151
  def set_metrics(
148
152
  worker_name: str,
149
- state: EdgeWorkerState,
153
+ state: str | EdgeWorkerState,
150
154
  jobs_active: int,
151
155
  concurrency: int,
152
156
  free_concurrency: int,
@@ -204,7 +208,7 @@ def reset_metrics(worker_name: str) -> None:
204
208
  @provide_session
205
209
  def _fetch_edge_hosts_from_db(
206
210
  hostname: str | None = None, states: list | None = None, session: Session = NEW_SESSION
207
- ) -> list:
211
+ ) -> Sequence[EdgeWorkerModel]:
208
212
  query = select(EdgeWorkerModel)
209
213
  if states:
210
214
  query = query.where(EdgeWorkerModel.state.in_(states))
@@ -886,8 +886,6 @@ components:
886
886
  token:
887
887
  type: string
888
888
  title: Token
889
- ti:
890
- $ref: '#/components/schemas/TaskInstance'
891
889
  dag_rel_path:
892
890
  type: string
893
891
  format: path
@@ -899,6 +897,8 @@ components:
899
897
  - type: string
900
898
  - type: 'null'
901
899
  title: Log Path
900
+ ti:
901
+ $ref: '#/components/schemas/TaskInstance'
902
902
  type:
903
903
  type: string
904
904
  const: ExecuteTask
@@ -907,10 +907,10 @@ components:
907
907
  type: object
908
908
  required:
909
909
  - token
910
- - ti
911
910
  - dag_rel_path
912
911
  - bundle_info
913
912
  - log_path
913
+ - ti
914
914
  title: ExecuteTask
915
915
  description: Execute the given Task.
916
916
  HTTPExceptionResponse:
@@ -227,6 +227,22 @@ else:
227
227
  RUNNING_ON_APISERVER = "gunicorn" in sys.argv[0] and "airflow-webserver" in sys.argv
228
228
 
229
229
 
230
+ def _get_base_url_path(path: str) -> str:
231
+ """Construct URL path with webserver base_url prefix."""
232
+ base_url = conf.get("api", "base_url", fallback="/")
233
+ # Extract pathname from base_url (handles both full URLs and path-only)
234
+ if base_url.startswith(("http://", "https://")):
235
+ from urllib.parse import urlparse
236
+
237
+ base_path = urlparse(base_url).path
238
+ else:
239
+ base_path = base_url
240
+
241
+ # Normalize paths: remove trailing slash from base, ensure leading slash on path
242
+ base_path = base_path.rstrip("/")
243
+ return base_path + path
244
+
245
+
230
246
  class EdgeExecutorPlugin(AirflowPlugin):
231
247
  """EdgeExecutor Plugin - provides API endpoints for Edge Workers in Webserver."""
232
248
 
@@ -237,30 +253,30 @@ class EdgeExecutorPlugin(AirflowPlugin):
237
253
  react_apps = [
238
254
  {
239
255
  "name": "Edge Worker",
240
- "bundle_url": "/edge_worker/static/main.umd.cjs",
256
+ "bundle_url": _get_base_url_path("/edge_worker/static/main.umd.cjs"),
241
257
  "destination": "nav",
242
258
  "url_route": "edge_worker",
243
259
  "category": "admin",
244
- "icon": "/edge_worker/res/cloud-computer.svg",
245
- "icon_dark_mode": "/edge_worker/res/cloud-computer-dark.svg",
260
+ "icon": _get_base_url_path("/edge_worker/res/cloud-computer.svg"),
261
+ "icon_dark_mode": _get_base_url_path("/edge_worker/res/cloud-computer-dark.svg"),
246
262
  },
247
263
  {
248
264
  "name": "Edge Worker Jobs",
249
- "bundle_url": "/edge_worker/static/main.umd.cjs",
265
+ "bundle_url": _get_base_url_path("/edge_worker/static/main.umd.cjs"),
250
266
  "url_route": "edge_jobs",
251
267
  "category": "admin",
252
- "icon": "/edge_worker/res/cloud-computer.svg",
253
- "icon_dark_mode": "/edge_worker/res/cloud-computer-dark.svg",
268
+ "icon": _get_base_url_path("/edge_worker/res/cloud-computer.svg"),
269
+ "icon_dark_mode": _get_base_url_path("/edge_worker/res/cloud-computer-dark.svg"),
254
270
  },
255
271
  ]
256
272
  external_views = [
257
273
  {
258
274
  "name": "Edge Worker API docs",
259
- "href": "/edge_worker/docs",
275
+ "href": _get_base_url_path("/edge_worker/docs"),
260
276
  "destination": "nav",
261
277
  "category": "docs",
262
- "icon": "/edge_worker/res/cloud-computer.svg",
263
- "icon_dark_mode": "/edge_worker/res/cloud-computer-dark.svg",
278
+ "icon": _get_base_url_path("/edge_worker/res/cloud-computer.svg"),
279
+ "icon_dark_mode": _get_base_url_path("/edge_worker/res/cloud-computer-dark.svg"),
264
280
  "url_route": "edge_worker_api_docs",
265
281
  }
266
282
  ]
@@ -271,7 +287,7 @@ class EdgeExecutorPlugin(AirflowPlugin):
271
287
  appbuilder_menu_items = [
272
288
  {
273
289
  "name": "Edge Worker API docs",
274
- "href": "/edge_worker/v1/ui",
290
+ "href": _get_base_url_path("/edge_worker/v1/ui"),
275
291
  "category": "Docs",
276
292
  }
277
293
  ]