apache-airflow-providers-edge3 1.2.0rc1__py3-none-any.whl → 1.3.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.
Files changed (37) hide show
  1. airflow/providers/edge3/__init__.py +1 -1
  2. airflow/providers/edge3/cli/edge_command.py +43 -0
  3. airflow/providers/edge3/cli/worker.py +40 -40
  4. airflow/providers/edge3/models/edge_worker.py +13 -8
  5. airflow/providers/edge3/openapi/v2-edge-generated.yaml +249 -0
  6. airflow/providers/edge3/plugins/www/dist/main.umd.cjs +53 -19
  7. airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts +7 -0
  8. airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts +44 -1
  9. airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts +14 -0
  10. airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts +158 -1
  11. airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts +155 -0
  12. airflow/providers/edge3/plugins/www/package.json +14 -10
  13. airflow/providers/edge3/plugins/www/pnpm-lock.yaml +601 -457
  14. airflow/providers/edge3/plugins/www/src/components/AddQueueButton.tsx +138 -0
  15. airflow/providers/edge3/plugins/www/src/components/MaintenanceEditCommentButton.tsx +106 -0
  16. airflow/providers/edge3/plugins/www/src/components/MaintenanceEnterButton.tsx +102 -0
  17. airflow/providers/edge3/plugins/www/src/components/MaintenanceExitButton.tsx +92 -0
  18. airflow/providers/edge3/plugins/www/src/components/RemoveQueueButton.tsx +151 -0
  19. airflow/providers/edge3/plugins/www/src/components/WorkerDeleteButton.tsx +104 -0
  20. airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx +85 -0
  21. airflow/providers/edge3/plugins/www/src/components/WorkerShutdownButton.tsx +104 -0
  22. airflow/providers/edge3/plugins/www/src/components/WorkerStateBadge.tsx +33 -0
  23. airflow/providers/edge3/plugins/www/src/components/ui/ScrollToAnchor.tsx +49 -0
  24. airflow/providers/edge3/plugins/www/src/components/ui/createToaster.ts +24 -0
  25. airflow/providers/edge3/plugins/www/src/components/ui/index.ts +2 -0
  26. airflow/providers/edge3/plugins/www/src/context/colorMode/ColorModeProvider.tsx +1 -2
  27. airflow/providers/edge3/plugins/www/src/context/colorMode/useColorMode.tsx +2 -5
  28. airflow/providers/edge3/plugins/www/src/pages/JobsPage.tsx +52 -15
  29. airflow/providers/edge3/plugins/www/src/pages/WorkerPage.tsx +52 -22
  30. airflow/providers/edge3/plugins/www/src/theme.ts +378 -130
  31. airflow/providers/edge3/plugins/www/vite.config.ts +2 -0
  32. airflow/providers/edge3/worker_api/datamodels_ui.py +12 -0
  33. airflow/providers/edge3/worker_api/routes/ui.py +193 -3
  34. {apache_airflow_providers_edge3-1.2.0rc1.dist-info → apache_airflow_providers_edge3-1.3.0.dist-info}/METADATA +7 -7
  35. {apache_airflow_providers_edge3-1.2.0rc1.dist-info → apache_airflow_providers_edge3-1.3.0.dist-info}/RECORD +37 -27
  36. {apache_airflow_providers_edge3-1.2.0rc1.dist-info → apache_airflow_providers_edge3-1.3.0.dist-info}/WHEEL +0 -0
  37. {apache_airflow_providers_edge3-1.2.0rc1.dist-info → apache_airflow_providers_edge3-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -29,7 +29,7 @@ from airflow import __version__ as airflow_version
29
29
 
30
30
  __all__ = ["__version__"]
31
31
 
32
- __version__ = "1.2.0"
32
+ __version__ = "1.3.0"
33
33
 
34
34
  if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
35
35
  "2.10.0"
@@ -351,6 +351,37 @@ def remote_worker_request_shutdown(args) -> None:
351
351
  logger.info("Requested shutdown of Edge Worker host %s by %s.", args.edge_hostname, getuser())
352
352
 
353
353
 
354
+ @cli_utils.action_cli(check_db=False)
355
+ @providers_configuration_loaded
356
+ def shutdown_all_workers(args) -> None:
357
+ """Request graceful shutdown of all edge workers."""
358
+ _check_valid_db_connection()
359
+ if not (
360
+ args.yes
361
+ or input("This will shutdown all active edge workers, this cannot be undone! Proceed? (y/n)").upper()
362
+ == "Y"
363
+ ):
364
+ raise SystemExit("Cancelled")
365
+
366
+ from airflow.providers.edge3.models.edge_worker import get_registered_edge_hosts, request_shutdown
367
+
368
+ all_hosts = list(get_registered_edge_hosts())
369
+ if not all_hosts:
370
+ logger.info("No edge workers found to shutdown.")
371
+ return
372
+
373
+ shutdown_count = 0
374
+ for host in all_hosts:
375
+ try:
376
+ request_shutdown(host.worker_name)
377
+ logger.info("Requested shutdown of Edge Worker host %s", host.worker_name)
378
+ shutdown_count += 1
379
+ except Exception as e:
380
+ logger.error("Failed to shutdown Edge Worker host %s: %s", host.worker_name, e)
381
+
382
+ logger.info("Requested shutdown of %d edge workers by %s.", shutdown_count, getuser())
383
+
384
+
354
385
  @cli_utils.action_cli(check_db=False)
355
386
  @providers_configuration_loaded
356
387
  def add_worker_queues(args) -> None:
@@ -468,6 +499,12 @@ ARG_UMASK = Arg(
468
499
  ARG_STDERR = Arg(("--stderr",), help="Redirect stderr to this file if run in daemon mode")
469
500
  ARG_STDOUT = Arg(("--stdout",), help="Redirect stdout to this file if run in daemon mode")
470
501
  ARG_LOG_FILE = Arg(("-l", "--log-file"), help="Location of the log file if run in daemon mode")
502
+ ARG_YES = Arg(
503
+ ("-y", "--yes"),
504
+ help="Skip confirmation prompt and proceed with shutdown",
505
+ action="store_true",
506
+ default=False,
507
+ )
471
508
 
472
509
  EDGE_COMMANDS: list[ActionCommand] = [
473
510
  ActionCommand(
@@ -581,4 +618,10 @@ EDGE_COMMANDS: list[ActionCommand] = [
581
618
  ARG_QUEUES_MANAGE,
582
619
  ),
583
620
  ),
621
+ ActionCommand(
622
+ name="shutdown-all-workers",
623
+ help=shutdown_all_workers.__doc__,
624
+ func=shutdown_all_workers,
625
+ args=(ARG_YES,),
626
+ ),
584
627
  ]
@@ -175,48 +175,46 @@ class EdgeWorker:
175
175
  return EdgeWorkerState.MAINTENANCE_MODE
176
176
  return EdgeWorkerState.IDLE
177
177
 
178
- def _launch_job_af3(self, edge_job: EdgeJobFetched) -> tuple[Process, Path]:
179
- if TYPE_CHECKING:
180
- from airflow.executors.workloads import ExecuteTask
178
+ @staticmethod
179
+ def _run_job_via_supervisor(workload) -> int:
180
+ from airflow.sdk.execution_time.supervisor import supervise
181
181
 
182
- def _run_job_via_supervisor(
183
- workload: ExecuteTask,
184
- ) -> int:
185
- from airflow.sdk.execution_time.supervisor import supervise
182
+ # Ignore ctrl-c in this process -- we don't want to kill _this_ one. we let tasks run to completion
183
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
186
184
 
187
- # Ignore ctrl-c in this process -- we don't want to kill _this_ one. we let tasks run to completion
188
- signal.signal(signal.SIGINT, signal.SIG_IGN)
185
+ logger.info("Worker starting up pid=%d", os.getpid())
186
+ setproctitle(f"airflow edge worker: {workload.ti.key}")
189
187
 
190
- logger.info("Worker starting up pid=%d", os.getpid())
191
- setproctitle(f"airflow edge worker: {workload.ti.key}")
188
+ try:
189
+ api_url = conf.get("edge", "api_url")
190
+ execution_api_server_url = conf.get("core", "execution_api_server_url", fallback="")
191
+ if not execution_api_server_url:
192
+ parsed = urlparse(api_url)
193
+ execution_api_server_url = f"{parsed.scheme}://{parsed.netloc}/execution/"
194
+
195
+ supervise(
196
+ # This is the "wrong" ti type, but it duck types the same. TODO: Create a protocol for this.
197
+ # Same like in airflow/executors/local_executor.py:_execute_work()
198
+ ti=workload.ti, # type: ignore[arg-type]
199
+ dag_rel_path=workload.dag_rel_path,
200
+ bundle_info=workload.bundle_info,
201
+ token=workload.token,
202
+ server=execution_api_server_url,
203
+ log_path=workload.log_path,
204
+ )
205
+ return 0
206
+ except Exception as e:
207
+ logger.exception("Task execution failed: %s", e)
208
+ return 1
192
209
 
193
- try:
194
- api_url = conf.get("edge", "api_url")
195
- execution_api_server_url = conf.get("core", "execution_api_server_url", fallback="")
196
- if not execution_api_server_url:
197
- parsed = urlparse(api_url)
198
- execution_api_server_url = f"{parsed.scheme}://{parsed.netloc}/execution/"
199
-
200
- logger.info("Worker starting up server=execution_api_server_url=%s", execution_api_server_url)
201
-
202
- supervise(
203
- # This is the "wrong" ti type, but it duck types the same. TODO: Create a protocol for this.
204
- # Same like in airflow/executors/local_executor.py:_execute_work()
205
- ti=workload.ti, # type: ignore[arg-type]
206
- dag_rel_path=workload.dag_rel_path,
207
- bundle_info=workload.bundle_info,
208
- token=workload.token,
209
- server=execution_api_server_url,
210
- log_path=workload.log_path,
211
- )
212
- return 0
213
- except Exception as e:
214
- logger.exception("Task execution failed: %s", e)
215
- return 1
210
+ @staticmethod
211
+ def _launch_job_af3(edge_job: EdgeJobFetched) -> tuple[Process, Path]:
212
+ if TYPE_CHECKING:
213
+ from airflow.executors.workloads import ExecuteTask
216
214
 
217
215
  workload: ExecuteTask = edge_job.command
218
216
  process = Process(
219
- target=_run_job_via_supervisor,
217
+ target=EdgeWorker._run_job_via_supervisor,
220
218
  kwargs={"workload": workload},
221
219
  )
222
220
  process.start()
@@ -226,7 +224,8 @@ class EdgeWorker:
226
224
  logfile = Path(base_log_folder, workload.log_path)
227
225
  return process, logfile
228
226
 
229
- def _launch_job_af2_10(self, edge_job: EdgeJobFetched) -> tuple[Popen, Path]:
227
+ @staticmethod
228
+ def _launch_job_af2_10(edge_job: EdgeJobFetched) -> tuple[Popen, Path]:
230
229
  """Compatibility for Airflow 2.10 Launch."""
231
230
  env = os.environ.copy()
232
231
  env["AIRFLOW__CORE__DATABASE_ACCESS_ISOLATION"] = "True"
@@ -237,14 +236,15 @@ class EdgeWorker:
237
236
  logfile = logs_logfile_path(edge_job.key)
238
237
  return process, logfile
239
238
 
240
- def _launch_job(self, edge_job: EdgeJobFetched):
239
+ @staticmethod
240
+ def _launch_job(edge_job: EdgeJobFetched):
241
241
  """Get the received job executed."""
242
242
  process: Popen | Process
243
243
  if AIRFLOW_V_3_0_PLUS:
244
- process, logfile = self._launch_job_af3(edge_job)
244
+ process, logfile = EdgeWorker._launch_job_af3(edge_job)
245
245
  else:
246
246
  # Airflow 2.10
247
- process, logfile = self._launch_job_af2_10(edge_job)
247
+ process, logfile = EdgeWorker._launch_job_af2_10(edge_job)
248
248
  EdgeWorker.jobs.append(Job(edge_job, process, logfile, 0))
249
249
 
250
250
  def start(self):
@@ -316,7 +316,7 @@ class EdgeWorker:
316
316
  edge_job = jobs_fetch(self.hostname, self.queues, self.free_concurrency)
317
317
  if edge_job:
318
318
  logger.info("Received job: %s", edge_job)
319
- self._launch_job(edge_job)
319
+ EdgeWorker._launch_job(edge_job)
320
320
  jobs_set_state(edge_job.key, TaskInstanceState.RUNNING)
321
321
  return True
322
322
 
@@ -56,21 +56,21 @@ class EdgeWorkerState(str, Enum):
56
56
  IDLE = "idle"
57
57
  """Edge Worker is active and waiting for a task."""
58
58
  SHUTDOWN_REQUEST = "shutdown request"
59
- """Request to shutdown Edge Worker."""
59
+ """Request to shutdown Edge Worker is issued. It will be picked-up on the next heartbeat, tasks will drain and then worker will terminate."""
60
60
  TERMINATING = "terminating"
61
- """Edge Worker is completing work and stopping."""
61
+ """Edge Worker is completing work (draining running tasks) and stopping."""
62
62
  OFFLINE = "offline"
63
63
  """Edge Worker was shut down."""
64
64
  UNKNOWN = "unknown"
65
- """No heartbeat signal from worker for some time, Edge Worker probably down."""
65
+ """No heartbeat signal from worker for some time, Edge Worker probably down or got disconnected."""
66
66
  MAINTENANCE_REQUEST = "maintenance request"
67
- """Worker was requested to enter maintenance mode. Once worker receives this it will pause fetching jobs."""
67
+ """Worker was requested to enter maintenance mode. Once worker receives this message it will pause fetching tasks and drain tasks."""
68
68
  MAINTENANCE_PENDING = "maintenance pending"
69
- """Edge worker received the request for maintenance, waiting for jobs to finish. Once jobs are finished will move to 'maintenance mode'."""
69
+ """Edge Worker received the request for maintenance, waiting for tasks to finish. Once tasks are finished will move to 'maintenance mode'."""
70
70
  MAINTENANCE_MODE = "maintenance mode"
71
- """Edge worker is in maintenance mode. It is online but pauses fetching jobs."""
71
+ """Edge Worker is in maintenance mode. It is online but pauses fetching tasks."""
72
72
  MAINTENANCE_EXIT = "maintenance exit"
73
- """Request worker to exit maintenance mode. Once the worker receives this state it will un-pause and fetch new jobs."""
73
+ """Request Worker is requested to exit maintenance mode. Once the worker receives this state it will un-pause and fetch new tasks."""
74
74
  OFFLINE_MAINTENANCE = "offline maintenance"
75
75
  """Worker was shut down in maintenance mode. It will be in maintenance mode when restarted."""
76
76
 
@@ -264,7 +264,12 @@ def change_maintenance_comment(
264
264
  """Write maintenance comment in the db."""
265
265
  query = select(EdgeWorkerModel).where(EdgeWorkerModel.worker_name == worker_name)
266
266
  worker: EdgeWorkerModel = session.scalar(query)
267
- if worker.state in (EdgeWorkerState.MAINTENANCE_MODE, EdgeWorkerState.OFFLINE_MAINTENANCE):
267
+ if worker.state in (
268
+ EdgeWorkerState.MAINTENANCE_MODE,
269
+ EdgeWorkerState.MAINTENANCE_PENDING,
270
+ EdgeWorkerState.MAINTENANCE_REQUEST,
271
+ EdgeWorkerState.OFFLINE_MAINTENANCE,
272
+ ):
268
273
  worker.maintenance_comment = maintenance_comment
269
274
  else:
270
275
  error_message = f"Cannot change maintenance comment as {worker_name} is not in maintenance!"
@@ -586,6 +586,244 @@ paths:
586
586
  security:
587
587
  - OAuth2PasswordBearer: []
588
588
  - HTTPBearer: []
589
+ /edge_worker/ui/worker/{worker_name}/maintenance:
590
+ post:
591
+ tags:
592
+ - UI
593
+ summary: Request Worker Maintenance
594
+ description: Put a worker into maintenance mode.
595
+ operationId: request_worker_maintenance
596
+ security:
597
+ - OAuth2PasswordBearer: []
598
+ - HTTPBearer: []
599
+ parameters:
600
+ - name: worker_name
601
+ in: path
602
+ required: true
603
+ schema:
604
+ type: string
605
+ title: Worker Name
606
+ requestBody:
607
+ required: true
608
+ content:
609
+ application/json:
610
+ schema:
611
+ $ref: '#/components/schemas/MaintenanceRequest'
612
+ responses:
613
+ '200':
614
+ description: Successful Response
615
+ content:
616
+ application/json:
617
+ schema:
618
+ type: 'null'
619
+ title: Response Request Worker Maintenance
620
+ '422':
621
+ description: Validation Error
622
+ content:
623
+ application/json:
624
+ schema:
625
+ $ref: '#/components/schemas/HTTPValidationError'
626
+ patch:
627
+ tags:
628
+ - UI
629
+ summary: Update Worker Maintenance
630
+ description: Update maintenance comments for a worker.
631
+ operationId: update_worker_maintenance
632
+ security:
633
+ - OAuth2PasswordBearer: []
634
+ - HTTPBearer: []
635
+ parameters:
636
+ - name: worker_name
637
+ in: path
638
+ required: true
639
+ schema:
640
+ type: string
641
+ title: Worker Name
642
+ requestBody:
643
+ required: true
644
+ content:
645
+ application/json:
646
+ schema:
647
+ $ref: '#/components/schemas/MaintenanceRequest'
648
+ responses:
649
+ '200':
650
+ description: Successful Response
651
+ content:
652
+ application/json:
653
+ schema:
654
+ type: 'null'
655
+ title: Response Update Worker Maintenance
656
+ '422':
657
+ description: Validation Error
658
+ content:
659
+ application/json:
660
+ schema:
661
+ $ref: '#/components/schemas/HTTPValidationError'
662
+ delete:
663
+ tags:
664
+ - UI
665
+ summary: Exit Worker Maintenance
666
+ description: Exit a worker from maintenance mode.
667
+ operationId: exit_worker_maintenance
668
+ security:
669
+ - OAuth2PasswordBearer: []
670
+ - HTTPBearer: []
671
+ parameters:
672
+ - name: worker_name
673
+ in: path
674
+ required: true
675
+ schema:
676
+ type: string
677
+ title: Worker Name
678
+ responses:
679
+ '200':
680
+ description: Successful Response
681
+ content:
682
+ application/json:
683
+ schema:
684
+ type: 'null'
685
+ title: Response Exit Worker Maintenance
686
+ '422':
687
+ description: Validation Error
688
+ content:
689
+ application/json:
690
+ schema:
691
+ $ref: '#/components/schemas/HTTPValidationError'
692
+ /edge_worker/ui/worker/{worker_name}/shutdown:
693
+ post:
694
+ tags:
695
+ - UI
696
+ summary: Request Worker Shutdown
697
+ description: Request shutdown of a worker.
698
+ operationId: request_worker_shutdown
699
+ security:
700
+ - OAuth2PasswordBearer: []
701
+ - HTTPBearer: []
702
+ parameters:
703
+ - name: worker_name
704
+ in: path
705
+ required: true
706
+ schema:
707
+ type: string
708
+ title: Worker Name
709
+ responses:
710
+ '200':
711
+ description: Successful Response
712
+ content:
713
+ application/json:
714
+ schema:
715
+ type: 'null'
716
+ title: Response Request Worker Shutdown
717
+ '422':
718
+ description: Validation Error
719
+ content:
720
+ application/json:
721
+ schema:
722
+ $ref: '#/components/schemas/HTTPValidationError'
723
+ /edge_worker/ui/worker/{worker_name}:
724
+ delete:
725
+ tags:
726
+ - UI
727
+ summary: Delete Worker
728
+ description: Delete a worker record from the system.
729
+ operationId: delete_worker
730
+ security:
731
+ - OAuth2PasswordBearer: []
732
+ - HTTPBearer: []
733
+ parameters:
734
+ - name: worker_name
735
+ in: path
736
+ required: true
737
+ schema:
738
+ type: string
739
+ title: Worker Name
740
+ responses:
741
+ '200':
742
+ description: Successful Response
743
+ content:
744
+ application/json:
745
+ schema:
746
+ type: 'null'
747
+ title: Response Delete Worker
748
+ '422':
749
+ description: Validation Error
750
+ content:
751
+ application/json:
752
+ schema:
753
+ $ref: '#/components/schemas/HTTPValidationError'
754
+ /edge_worker/ui/worker/{worker_name}/queues/{queue_name}:
755
+ put:
756
+ tags:
757
+ - UI
758
+ summary: Add Worker Queue
759
+ description: Add a queue to a worker.
760
+ operationId: add_worker_queue
761
+ security:
762
+ - OAuth2PasswordBearer: []
763
+ - HTTPBearer: []
764
+ parameters:
765
+ - name: worker_name
766
+ in: path
767
+ required: true
768
+ schema:
769
+ type: string
770
+ title: Worker Name
771
+ - name: queue_name
772
+ in: path
773
+ required: true
774
+ schema:
775
+ type: string
776
+ title: Queue Name
777
+ responses:
778
+ '200':
779
+ description: Successful Response
780
+ content:
781
+ application/json:
782
+ schema:
783
+ type: 'null'
784
+ title: Response Add Worker Queue
785
+ '422':
786
+ description: Validation Error
787
+ content:
788
+ application/json:
789
+ schema:
790
+ $ref: '#/components/schemas/HTTPValidationError'
791
+ delete:
792
+ tags:
793
+ - UI
794
+ summary: Remove Worker Queue
795
+ description: Remove a queue from a worker.
796
+ operationId: remove_worker_queue
797
+ security:
798
+ - OAuth2PasswordBearer: []
799
+ - HTTPBearer: []
800
+ parameters:
801
+ - name: worker_name
802
+ in: path
803
+ required: true
804
+ schema:
805
+ type: string
806
+ title: Worker Name
807
+ - name: queue_name
808
+ in: path
809
+ required: true
810
+ schema:
811
+ type: string
812
+ title: Queue Name
813
+ responses:
814
+ '200':
815
+ description: Successful Response
816
+ content:
817
+ application/json:
818
+ schema:
819
+ type: 'null'
820
+ title: Response Remove Worker Queue
821
+ '422':
822
+ description: Validation Error
823
+ content:
824
+ application/json:
825
+ schema:
826
+ $ref: '#/components/schemas/HTTPValidationError'
589
827
  components:
590
828
  schemas:
591
829
  BundleInfo:
@@ -794,6 +1032,17 @@ components:
794
1032
  - total_entries
795
1033
  title: JobCollectionResponse
796
1034
  description: Job Collection serializer.
1035
+ MaintenanceRequest:
1036
+ properties:
1037
+ maintenance_comment:
1038
+ type: string
1039
+ title: Maintenance Comment
1040
+ description: Comment describing the maintenance reason.
1041
+ type: object
1042
+ required:
1043
+ - maintenance_comment
1044
+ title: MaintenanceRequest
1045
+ description: Request body for maintenance operations.
797
1046
  PushLogsBody:
798
1047
  properties:
799
1048
  log_chunk_time: