UncountablePythonSDK 0.0.83__py3-none-any.whl → 0.0.132__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 UncountablePythonSDK might be problematic. Click here for more details.
- docs/conf.py +54 -7
- docs/index.md +107 -4
- docs/integration_examples/create_ingredient.md +43 -0
- docs/integration_examples/create_output.md +56 -0
- docs/integration_examples/index.md +6 -0
- docs/justfile +2 -2
- docs/requirements.txt +6 -4
- examples/basic_auth.py +7 -0
- examples/create_ingredient_sdk.py +34 -0
- examples/download_files.py +26 -0
- examples/integration-server/jobs/materials_auto/concurrent_cron.py +11 -0
- examples/integration-server/jobs/materials_auto/example_cron.py +3 -0
- examples/integration-server/jobs/materials_auto/example_http.py +47 -0
- examples/integration-server/jobs/materials_auto/example_instrument.py +100 -0
- examples/integration-server/jobs/materials_auto/example_parse.py +140 -0
- examples/integration-server/jobs/materials_auto/example_predictions.py +61 -0
- examples/integration-server/jobs/materials_auto/example_runsheet_wh.py +39 -0
- examples/integration-server/jobs/materials_auto/example_wh.py +17 -9
- examples/integration-server/jobs/materials_auto/profile.yaml +61 -0
- examples/integration-server/pyproject.toml +10 -10
- examples/oauth.py +7 -0
- examples/set_recipe_metadata_file.py +1 -1
- examples/upload_files.py +1 -2
- pkgs/argument_parser/__init__.py +8 -0
- pkgs/argument_parser/_is_namedtuple.py +3 -0
- pkgs/argument_parser/argument_parser.py +196 -63
- pkgs/filesystem_utils/__init__.py +1 -0
- pkgs/filesystem_utils/_blob_session.py +144 -0
- pkgs/filesystem_utils/_gdrive_session.py +5 -5
- pkgs/filesystem_utils/_s3_session.py +2 -1
- pkgs/filesystem_utils/_sftp_session.py +6 -3
- pkgs/filesystem_utils/file_type_utils.py +30 -10
- pkgs/serialization/__init__.py +7 -2
- pkgs/serialization/annotation.py +64 -0
- pkgs/serialization/missing_sentry.py +1 -1
- pkgs/serialization/opaque_key.py +1 -1
- pkgs/serialization/serial_alias.py +47 -0
- pkgs/serialization/serial_class.py +40 -48
- pkgs/serialization/serial_generic.py +16 -0
- pkgs/serialization/serial_union.py +16 -16
- pkgs/serialization_util/__init__.py +6 -0
- pkgs/serialization_util/dataclasses.py +14 -0
- pkgs/serialization_util/serialization_helpers.py +15 -5
- pkgs/type_spec/actions_registry/__main__.py +0 -4
- pkgs/type_spec/actions_registry/emit_typescript.py +2 -4
- pkgs/type_spec/builder.py +248 -70
- pkgs/type_spec/builder_types.py +9 -0
- pkgs/type_spec/config.py +40 -7
- pkgs/type_spec/cross_output_links.py +99 -0
- pkgs/type_spec/emit_open_api.py +121 -34
- pkgs/type_spec/emit_open_api_util.py +5 -5
- pkgs/type_spec/emit_python.py +277 -86
- pkgs/type_spec/emit_typescript.py +102 -29
- pkgs/type_spec/emit_typescript_util.py +66 -10
- pkgs/type_spec/load_types.py +16 -3
- pkgs/type_spec/non_discriminated_union_exceptions.py +14 -0
- pkgs/type_spec/open_api_util.py +29 -4
- pkgs/type_spec/parts/base.py.prepart +11 -8
- pkgs/type_spec/parts/base.ts.prepart +4 -0
- pkgs/type_spec/type_info/__main__.py +3 -1
- pkgs/type_spec/type_info/emit_type_info.py +115 -22
- pkgs/type_spec/ui_entry_actions/__init__.py +4 -0
- pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +308 -0
- pkgs/type_spec/util.py +3 -3
- pkgs/type_spec/value_spec/__main__.py +26 -9
- pkgs/type_spec/value_spec/convert_type.py +18 -0
- pkgs/type_spec/value_spec/emit_python.py +13 -3
- pkgs/type_spec/value_spec/types.py +1 -1
- uncountable/core/async_batch.py +1 -1
- uncountable/core/client.py +133 -34
- uncountable/core/environment.py +3 -3
- uncountable/core/file_upload.py +39 -15
- uncountable/integration/cli.py +116 -23
- uncountable/integration/construct_client.py +3 -3
- uncountable/integration/executors/executors.py +12 -2
- uncountable/integration/executors/generic_upload_executor.py +66 -14
- uncountable/integration/http_server/__init__.py +5 -0
- uncountable/integration/http_server/types.py +69 -0
- uncountable/integration/job.py +192 -7
- uncountable/integration/queue_runner/command_server/__init__.py +4 -0
- uncountable/integration/queue_runner/command_server/command_client.py +65 -0
- uncountable/integration/queue_runner/command_server/command_server.py +83 -5
- uncountable/integration/queue_runner/command_server/constants.py +4 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server.proto +36 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +28 -11
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +77 -1
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +135 -0
- uncountable/integration/queue_runner/command_server/types.py +25 -2
- uncountable/integration/queue_runner/datastore/datastore_sqlite.py +168 -11
- uncountable/integration/queue_runner/datastore/interface.py +10 -0
- uncountable/integration/queue_runner/datastore/model.py +8 -1
- uncountable/integration/queue_runner/job_scheduler.py +63 -23
- uncountable/integration/queue_runner/queue_runner.py +10 -2
- uncountable/integration/queue_runner/worker.py +3 -5
- uncountable/integration/scan_profiles.py +1 -1
- uncountable/integration/scheduler.py +74 -25
- uncountable/integration/secret_retrieval/retrieve_secret.py +1 -1
- uncountable/integration/server.py +42 -12
- uncountable/integration/telemetry.py +63 -10
- uncountable/integration/webhook_server/entrypoint.py +39 -112
- uncountable/types/__init__.py +58 -1
- uncountable/types/api/batch/execute_batch.py +5 -6
- uncountable/types/api/batch/execute_batch_load_async.py +2 -3
- uncountable/types/api/chemical/convert_chemical_formats.py +10 -5
- uncountable/types/api/condition_parameters/__init__.py +1 -0
- uncountable/types/api/condition_parameters/upsert_condition_match.py +72 -0
- uncountable/types/api/entity/create_entities.py +7 -7
- uncountable/types/api/entity/create_entity.py +8 -8
- uncountable/types/api/entity/create_or_update_entity.py +48 -0
- uncountable/types/api/entity/export_entities.py +59 -0
- uncountable/types/api/entity/get_entities_data.py +3 -4
- uncountable/types/api/entity/grant_entity_permissions.py +6 -6
- uncountable/types/api/entity/list_aggregate.py +79 -0
- uncountable/types/api/entity/list_entities.py +34 -10
- uncountable/types/api/entity/lock_entity.py +4 -4
- uncountable/types/api/entity/lookup_entity.py +116 -0
- uncountable/types/api/entity/resolve_entity_ids.py +5 -6
- uncountable/types/api/entity/set_entity_field_values.py +44 -0
- uncountable/types/api/entity/set_values.py +3 -3
- uncountable/types/api/entity/transition_entity_phase.py +14 -7
- uncountable/types/api/entity/unlock_entity.py +3 -3
- uncountable/types/api/equipment/associate_equipment_input.py +2 -3
- uncountable/types/api/field_options/upsert_field_options.py +7 -7
- uncountable/types/api/files/__init__.py +1 -0
- uncountable/types/api/files/download_file.py +77 -0
- uncountable/types/api/id_source/list_id_source.py +6 -7
- uncountable/types/api/id_source/match_id_source.py +4 -5
- uncountable/types/api/input_groups/get_input_group_names.py +3 -4
- uncountable/types/api/inputs/create_inputs.py +10 -9
- uncountable/types/api/inputs/get_input_data.py +11 -12
- uncountable/types/api/inputs/get_input_names.py +6 -7
- uncountable/types/api/inputs/get_inputs_data.py +6 -7
- uncountable/types/api/inputs/set_input_attribute_values.py +5 -6
- uncountable/types/api/inputs/set_input_category.py +5 -5
- uncountable/types/api/inputs/set_input_subcategories.py +3 -3
- uncountable/types/api/inputs/set_intermediate_type.py +4 -4
- uncountable/types/api/integrations/__init__.py +1 -0
- uncountable/types/api/integrations/publish_realtime_data.py +41 -0
- uncountable/types/api/integrations/push_notification.py +49 -0
- uncountable/types/api/integrations/register_sockets_token.py +41 -0
- uncountable/types/api/listing/__init__.py +1 -0
- uncountable/types/api/listing/fetch_listing.py +58 -0
- uncountable/types/api/material_families/update_entity_material_families.py +3 -4
- uncountable/types/api/notebooks/__init__.py +1 -0
- uncountable/types/api/notebooks/add_notebook_content.py +119 -0
- uncountable/types/api/outputs/get_output_data.py +12 -13
- uncountable/types/api/outputs/get_output_names.py +5 -6
- uncountable/types/api/outputs/get_output_organization.py +173 -0
- uncountable/types/api/outputs/resolve_output_conditions.py +7 -8
- uncountable/types/api/permissions/set_core_permissions.py +16 -10
- uncountable/types/api/project/get_projects.py +6 -7
- uncountable/types/api/project/get_projects_data.py +7 -8
- uncountable/types/api/recipe_links/create_recipe_link.py +5 -5
- uncountable/types/api/recipe_links/remove_recipe_link.py +4 -4
- uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +6 -7
- uncountable/types/api/recipes/add_recipe_to_project.py +3 -3
- uncountable/types/api/recipes/add_time_series_data.py +64 -0
- uncountable/types/api/recipes/archive_recipes.py +4 -4
- uncountable/types/api/recipes/associate_recipe_as_input.py +5 -5
- uncountable/types/api/recipes/associate_recipe_as_lot.py +3 -3
- uncountable/types/api/recipes/clear_recipe_outputs.py +3 -3
- uncountable/types/api/recipes/create_mix_order.py +44 -0
- uncountable/types/api/recipes/create_recipe.py +8 -9
- uncountable/types/api/recipes/create_recipes.py +8 -9
- uncountable/types/api/recipes/disassociate_recipe_as_input.py +3 -3
- uncountable/types/api/recipes/edit_recipe_inputs.py +101 -24
- uncountable/types/api/recipes/get_column_calculation_values.py +4 -5
- uncountable/types/api/recipes/get_curve.py +4 -5
- uncountable/types/api/recipes/get_recipe_calculations.py +6 -7
- uncountable/types/api/recipes/get_recipe_links.py +3 -4
- uncountable/types/api/recipes/get_recipe_names.py +3 -4
- uncountable/types/api/recipes/get_recipe_output_metadata.py +5 -6
- uncountable/types/api/recipes/get_recipes_data.py +62 -34
- uncountable/types/api/recipes/lock_recipes.py +9 -8
- uncountable/types/api/recipes/remove_recipe_from_project.py +3 -3
- uncountable/types/api/recipes/set_recipe_inputs.py +9 -10
- uncountable/types/api/recipes/set_recipe_metadata.py +3 -3
- uncountable/types/api/recipes/set_recipe_output_annotations.py +11 -12
- uncountable/types/api/recipes/set_recipe_output_file.py +5 -6
- uncountable/types/api/recipes/set_recipe_outputs.py +24 -13
- uncountable/types/api/recipes/set_recipe_tags.py +14 -9
- uncountable/types/api/recipes/set_recipe_total.py +59 -0
- uncountable/types/api/recipes/unarchive_recipes.py +3 -3
- uncountable/types/api/recipes/unlock_recipes.py +7 -6
- uncountable/types/api/runsheet/__init__.py +1 -0
- uncountable/types/api/runsheet/complete_async_upload.py +41 -0
- uncountable/types/api/triggers/run_trigger.py +4 -4
- uncountable/types/api/uploader/complete_async_parse.py +46 -0
- uncountable/types/api/uploader/invoke_uploader.py +4 -5
- uncountable/types/api/user/__init__.py +1 -0
- uncountable/types/api/user/get_current_user_info.py +40 -0
- uncountable/types/async_batch.py +1 -1
- uncountable/types/async_batch_processor.py +506 -23
- uncountable/types/async_batch_t.py +35 -8
- uncountable/types/async_jobs.py +0 -1
- uncountable/types/async_jobs_t.py +1 -2
- uncountable/types/auth_retrieval.py +0 -1
- uncountable/types/auth_retrieval_t.py +6 -6
- uncountable/types/base.py +0 -1
- uncountable/types/base_t.py +11 -9
- uncountable/types/calculations.py +0 -1
- uncountable/types/calculations_t.py +1 -2
- uncountable/types/chemical_structure.py +0 -1
- uncountable/types/chemical_structure_t.py +5 -5
- uncountable/types/client_base.py +614 -69
- uncountable/types/client_config.py +1 -1
- uncountable/types/client_config_t.py +13 -3
- uncountable/types/curves.py +0 -1
- uncountable/types/curves_t.py +6 -7
- uncountable/types/data.py +12 -0
- uncountable/types/data_t.py +103 -0
- uncountable/types/entity.py +1 -1
- uncountable/types/entity_t.py +90 -10
- uncountable/types/experiment_groups.py +0 -1
- uncountable/types/experiment_groups_t.py +1 -2
- uncountable/types/exports.py +8 -0
- uncountable/types/exports_t.py +34 -0
- uncountable/types/field_values.py +19 -1
- uncountable/types/field_values_t.py +242 -9
- uncountable/types/fields.py +0 -1
- uncountable/types/fields_t.py +1 -2
- uncountable/types/generic_upload.py +0 -1
- uncountable/types/generic_upload_t.py +14 -14
- uncountable/types/id_source.py +0 -1
- uncountable/types/id_source_t.py +13 -7
- uncountable/types/identifier.py +0 -1
- uncountable/types/identifier_t.py +10 -5
- uncountable/types/input_attributes.py +0 -1
- uncountable/types/input_attributes_t.py +3 -4
- uncountable/types/inputs.py +0 -1
- uncountable/types/inputs_t.py +3 -4
- uncountable/types/integration_server.py +0 -1
- uncountable/types/integration_server_t.py +13 -4
- uncountable/types/integration_session.py +10 -0
- uncountable/types/integration_session_t.py +60 -0
- uncountable/types/integrations.py +10 -0
- uncountable/types/integrations_t.py +62 -0
- uncountable/types/job_definition.py +2 -1
- uncountable/types/job_definition_t.py +57 -32
- uncountable/types/listing.py +9 -0
- uncountable/types/listing_t.py +51 -0
- uncountable/types/notices.py +8 -0
- uncountable/types/notices_t.py +37 -0
- uncountable/types/notifications.py +11 -0
- uncountable/types/notifications_t.py +74 -0
- uncountable/types/outputs.py +0 -1
- uncountable/types/outputs_t.py +2 -3
- uncountable/types/overrides.py +0 -1
- uncountable/types/overrides_t.py +10 -4
- uncountable/types/permissions.py +0 -1
- uncountable/types/permissions_t.py +1 -2
- uncountable/types/phases.py +0 -1
- uncountable/types/phases_t.py +1 -2
- uncountable/types/post_base.py +0 -1
- uncountable/types/post_base_t.py +1 -2
- uncountable/types/queued_job.py +2 -1
- uncountable/types/queued_job_t.py +29 -12
- uncountable/types/recipe_identifiers.py +0 -1
- uncountable/types/recipe_identifiers_t.py +18 -8
- uncountable/types/recipe_inputs.py +0 -1
- uncountable/types/recipe_inputs_t.py +1 -2
- uncountable/types/recipe_links.py +0 -1
- uncountable/types/recipe_links_t.py +3 -4
- uncountable/types/recipe_metadata.py +0 -1
- uncountable/types/recipe_metadata_t.py +9 -10
- uncountable/types/recipe_output_metadata.py +0 -1
- uncountable/types/recipe_output_metadata_t.py +1 -2
- uncountable/types/recipe_tags.py +0 -1
- uncountable/types/recipe_tags_t.py +1 -2
- uncountable/types/recipe_workflow_steps.py +0 -1
- uncountable/types/recipe_workflow_steps_t.py +7 -7
- uncountable/types/recipes.py +0 -1
- uncountable/types/recipes_t.py +2 -2
- uncountable/types/response.py +0 -1
- uncountable/types/response_t.py +2 -2
- uncountable/types/secret_retrieval.py +0 -1
- uncountable/types/secret_retrieval_t.py +7 -7
- uncountable/types/sockets.py +20 -0
- uncountable/types/sockets_t.py +169 -0
- uncountable/types/structured_filters.py +25 -0
- uncountable/types/structured_filters_t.py +248 -0
- uncountable/types/units.py +0 -1
- uncountable/types/units_t.py +1 -2
- uncountable/types/uploader.py +24 -0
- uncountable/types/uploader_t.py +222 -0
- uncountable/types/users.py +0 -1
- uncountable/types/users_t.py +1 -2
- uncountable/types/webhook_job.py +1 -1
- uncountable/types/webhook_job_t.py +14 -3
- uncountable/types/workflows.py +0 -1
- uncountable/types/workflows_t.py +3 -4
- uncountablepythonsdk-0.0.132.dist-info/METADATA +64 -0
- uncountablepythonsdk-0.0.132.dist-info/RECORD +363 -0
- {UncountablePythonSDK-0.0.83.dist-info → uncountablepythonsdk-0.0.132.dist-info}/WHEEL +1 -1
- UncountablePythonSDK-0.0.83.dist-info/METADATA +0 -60
- UncountablePythonSDK-0.0.83.dist-info/RECORD +0 -292
- docs/quickstart.md +0 -19
- {UncountablePythonSDK-0.0.83.dist-info → uncountablepythonsdk-0.0.132.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import datetime
|
|
1
2
|
import uuid
|
|
2
|
-
from datetime import
|
|
3
|
+
from datetime import UTC
|
|
3
4
|
|
|
4
|
-
from sqlalchemy import delete, insert, select, update
|
|
5
|
+
from sqlalchemy import delete, insert, or_, select, text, update
|
|
5
6
|
from sqlalchemy.engine import Engine
|
|
6
7
|
|
|
7
8
|
from pkgs.argument_parser import CachedParser
|
|
@@ -13,6 +14,8 @@ from uncountable.types import queued_job_t
|
|
|
13
14
|
|
|
14
15
|
queued_job_payload_parser = CachedParser(queued_job_t.QueuedJobPayload)
|
|
15
16
|
|
|
17
|
+
MAX_QUEUE_WINDOW_DAYS = 30
|
|
18
|
+
|
|
16
19
|
|
|
17
20
|
class DatastoreSqlite(Datastore):
|
|
18
21
|
def __init__(self, session_maker: DBSessionMaker) -> None:
|
|
@@ -22,6 +25,17 @@ class DatastoreSqlite(Datastore):
|
|
|
22
25
|
@classmethod
|
|
23
26
|
def setup(cls, engine: Engine) -> None:
|
|
24
27
|
Base.metadata.create_all(engine)
|
|
28
|
+
with engine.connect() as connection:
|
|
29
|
+
if not bool(
|
|
30
|
+
connection.execute(
|
|
31
|
+
text(
|
|
32
|
+
"select exists (select 1 from pragma_table_info('queued_jobs') where name='status');"
|
|
33
|
+
)
|
|
34
|
+
).scalar()
|
|
35
|
+
):
|
|
36
|
+
connection.execute(
|
|
37
|
+
text("alter table queued_jobs add column status VARCHAR")
|
|
38
|
+
)
|
|
25
39
|
|
|
26
40
|
def add_job_to_queue(
|
|
27
41
|
self, job_payload: queued_job_t.QueuedJobPayload, job_ref_name: str
|
|
@@ -30,11 +44,12 @@ class DatastoreSqlite(Datastore):
|
|
|
30
44
|
serialized_payload = serialize_for_storage(job_payload)
|
|
31
45
|
queued_job_uuid = str(uuid.uuid4())
|
|
32
46
|
num_attempts = 0
|
|
33
|
-
submitted_at = datetime.now(
|
|
47
|
+
submitted_at = datetime.datetime.now(UTC)
|
|
34
48
|
insert_stmt = insert(QueuedJob).values({
|
|
35
49
|
QueuedJob.id.key: queued_job_uuid,
|
|
36
50
|
QueuedJob.job_ref_name.key: job_ref_name,
|
|
37
51
|
QueuedJob.payload.key: serialized_payload,
|
|
52
|
+
QueuedJob.status.key: queued_job_t.JobStatus.QUEUED,
|
|
38
53
|
QueuedJob.num_attempts: num_attempts,
|
|
39
54
|
QueuedJob.submitted_at: submitted_at,
|
|
40
55
|
})
|
|
@@ -43,10 +58,48 @@ class DatastoreSqlite(Datastore):
|
|
|
43
58
|
queued_job_uuid=queued_job_uuid,
|
|
44
59
|
job_ref_name=job_ref_name,
|
|
45
60
|
payload=job_payload,
|
|
61
|
+
status=queued_job_t.JobStatus.QUEUED,
|
|
46
62
|
submitted_at=submitted_at,
|
|
47
63
|
num_attempts=num_attempts,
|
|
48
64
|
)
|
|
49
65
|
|
|
66
|
+
def retry_job(
|
|
67
|
+
self,
|
|
68
|
+
queued_job_uuid: str,
|
|
69
|
+
) -> queued_job_t.QueuedJob | None:
|
|
70
|
+
with self.session_maker() as session:
|
|
71
|
+
select_stmt = select(
|
|
72
|
+
QueuedJob.id,
|
|
73
|
+
QueuedJob.payload,
|
|
74
|
+
QueuedJob.num_attempts,
|
|
75
|
+
QueuedJob.job_ref_name,
|
|
76
|
+
QueuedJob.status,
|
|
77
|
+
QueuedJob.submitted_at,
|
|
78
|
+
).filter(QueuedJob.id == queued_job_uuid)
|
|
79
|
+
existing_job = session.execute(select_stmt).one_or_none()
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
existing_job is None
|
|
83
|
+
or existing_job.status != queued_job_t.JobStatus.FAILED
|
|
84
|
+
):
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
update_stmt = (
|
|
88
|
+
update(QueuedJob)
|
|
89
|
+
.values({QueuedJob.status.key: queued_job_t.JobStatus.QUEUED})
|
|
90
|
+
.filter(QueuedJob.id == queued_job_uuid)
|
|
91
|
+
)
|
|
92
|
+
session.execute(update_stmt)
|
|
93
|
+
|
|
94
|
+
return queued_job_t.QueuedJob(
|
|
95
|
+
queued_job_uuid=existing_job.id,
|
|
96
|
+
job_ref_name=existing_job.job_ref_name,
|
|
97
|
+
num_attempts=existing_job.num_attempts,
|
|
98
|
+
status=queued_job_t.JobStatus.QUEUED,
|
|
99
|
+
submitted_at=existing_job.submitted_at,
|
|
100
|
+
payload=queued_job_payload_parser.parse_storage(existing_job.payload),
|
|
101
|
+
)
|
|
102
|
+
|
|
50
103
|
def increment_num_attempts(self, queued_job_uuid: str) -> int:
|
|
51
104
|
with self.session_maker() as session:
|
|
52
105
|
update_stmt = (
|
|
@@ -56,7 +109,7 @@ class DatastoreSqlite(Datastore):
|
|
|
56
109
|
)
|
|
57
110
|
session.execute(update_stmt)
|
|
58
111
|
session.flush()
|
|
59
|
-
# IMPROVE: python3
|
|
112
|
+
# IMPROVE: python3's sqlite does not support the RETURNING clause
|
|
60
113
|
select_stmt = select(QueuedJob.num_attempts).filter(
|
|
61
114
|
QueuedJob.id == queued_job_uuid
|
|
62
115
|
)
|
|
@@ -67,15 +120,103 @@ class DatastoreSqlite(Datastore):
|
|
|
67
120
|
delete_stmt = delete(QueuedJob).filter(QueuedJob.id == queued_job_uuid)
|
|
68
121
|
session.execute(delete_stmt)
|
|
69
122
|
|
|
123
|
+
def update_job_status(
|
|
124
|
+
self, queued_job_uuid: str, status: queued_job_t.JobStatus
|
|
125
|
+
) -> None:
|
|
126
|
+
with self.session_maker() as session:
|
|
127
|
+
update_stmt = (
|
|
128
|
+
update(QueuedJob)
|
|
129
|
+
.values({QueuedJob.status.key: status})
|
|
130
|
+
.filter(QueuedJob.id == queued_job_uuid)
|
|
131
|
+
)
|
|
132
|
+
session.execute(update_stmt)
|
|
133
|
+
|
|
134
|
+
def list_queued_job_metadata(
|
|
135
|
+
self, offset: int = 0, limit: int | None = 100
|
|
136
|
+
) -> list[queued_job_t.QueuedJobMetadata]:
|
|
137
|
+
with self.session_maker() as session:
|
|
138
|
+
select_statement = (
|
|
139
|
+
select(
|
|
140
|
+
QueuedJob.id,
|
|
141
|
+
QueuedJob.job_ref_name,
|
|
142
|
+
QueuedJob.num_attempts,
|
|
143
|
+
QueuedJob.status,
|
|
144
|
+
QueuedJob.submitted_at,
|
|
145
|
+
)
|
|
146
|
+
.order_by(QueuedJob.submitted_at)
|
|
147
|
+
.offset(offset)
|
|
148
|
+
.limit(limit)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
queued_job_metadata: list[queued_job_t.QueuedJobMetadata] = [
|
|
152
|
+
queued_job_t.QueuedJobMetadata(
|
|
153
|
+
queued_job_uuid=row.id,
|
|
154
|
+
job_ref_name=row.job_ref_name,
|
|
155
|
+
num_attempts=row.num_attempts,
|
|
156
|
+
status=row.status or queued_job_t.JobStatus.QUEUED,
|
|
157
|
+
submitted_at=row.submitted_at,
|
|
158
|
+
)
|
|
159
|
+
for row in session.execute(select_statement)
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
return queued_job_metadata
|
|
163
|
+
|
|
164
|
+
def get_next_queued_job_for_ref_name(
|
|
165
|
+
self, job_ref_name: str
|
|
166
|
+
) -> queued_job_t.QueuedJob | None:
|
|
167
|
+
with self.session_maker() as session:
|
|
168
|
+
select_stmt = (
|
|
169
|
+
select(
|
|
170
|
+
QueuedJob.id,
|
|
171
|
+
QueuedJob.payload,
|
|
172
|
+
QueuedJob.num_attempts,
|
|
173
|
+
QueuedJob.job_ref_name,
|
|
174
|
+
QueuedJob.status,
|
|
175
|
+
QueuedJob.submitted_at,
|
|
176
|
+
)
|
|
177
|
+
.filter(QueuedJob.job_ref_name == job_ref_name)
|
|
178
|
+
.filter(
|
|
179
|
+
or_(
|
|
180
|
+
QueuedJob.status == queued_job_t.JobStatus.QUEUED,
|
|
181
|
+
QueuedJob.status.is_(None),
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
.limit(1)
|
|
185
|
+
.order_by(QueuedJob.submitted_at)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
for row in session.execute(select_stmt):
|
|
189
|
+
parsed_payload = queued_job_payload_parser.parse_storage(row.payload)
|
|
190
|
+
return queued_job_t.QueuedJob(
|
|
191
|
+
queued_job_uuid=row.id,
|
|
192
|
+
job_ref_name=row.job_ref_name,
|
|
193
|
+
num_attempts=row.num_attempts,
|
|
194
|
+
status=row.status or queued_job_t.JobStatus.QUEUED,
|
|
195
|
+
submitted_at=row.submitted_at,
|
|
196
|
+
payload=parsed_payload,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return None
|
|
200
|
+
|
|
70
201
|
def load_job_queue(self) -> list[queued_job_t.QueuedJob]:
|
|
71
202
|
with self.session_maker() as session:
|
|
72
|
-
select_stmt =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
203
|
+
select_stmt = (
|
|
204
|
+
select(
|
|
205
|
+
QueuedJob.id,
|
|
206
|
+
QueuedJob.payload,
|
|
207
|
+
QueuedJob.num_attempts,
|
|
208
|
+
QueuedJob.job_ref_name,
|
|
209
|
+
QueuedJob.status,
|
|
210
|
+
QueuedJob.submitted_at,
|
|
211
|
+
)
|
|
212
|
+
.filter(
|
|
213
|
+
or_(
|
|
214
|
+
QueuedJob.status == queued_job_t.JobStatus.QUEUED,
|
|
215
|
+
QueuedJob.status.is_(None),
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
.order_by(QueuedJob.submitted_at)
|
|
219
|
+
)
|
|
79
220
|
|
|
80
221
|
queued_jobs: list[queued_job_t.QueuedJob] = []
|
|
81
222
|
for row in session.execute(select_stmt):
|
|
@@ -85,9 +226,25 @@ class DatastoreSqlite(Datastore):
|
|
|
85
226
|
queued_job_uuid=row.id,
|
|
86
227
|
job_ref_name=row.job_ref_name,
|
|
87
228
|
num_attempts=row.num_attempts,
|
|
229
|
+
status=row.status or queued_job_t.JobStatus.QUEUED,
|
|
88
230
|
submitted_at=row.submitted_at,
|
|
89
231
|
payload=parsed_payload,
|
|
90
232
|
)
|
|
91
233
|
)
|
|
92
234
|
|
|
93
235
|
return queued_jobs
|
|
236
|
+
|
|
237
|
+
def vaccuum_queued_jobs(self) -> None:
|
|
238
|
+
with self.session_maker() as session:
|
|
239
|
+
delete_stmt = (
|
|
240
|
+
delete(QueuedJob)
|
|
241
|
+
.filter(QueuedJob.status == queued_job_t.JobStatus.QUEUED)
|
|
242
|
+
.filter(
|
|
243
|
+
QueuedJob.submitted_at
|
|
244
|
+
<= (
|
|
245
|
+
datetime.datetime.now(UTC)
|
|
246
|
+
- datetime.timedelta(days=MAX_QUEUE_WINDOW_DAYS)
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
session.execute(delete_stmt)
|
|
@@ -17,3 +17,13 @@ class Datastore(ABC):
|
|
|
17
17
|
|
|
18
18
|
@abstractmethod
|
|
19
19
|
def load_job_queue(self) -> list[queued_job_t.QueuedJob]: ...
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def get_next_queued_job_for_ref_name(
|
|
23
|
+
self, job_ref_name: str
|
|
24
|
+
) -> queued_job_t.QueuedJob | None: ...
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def list_queued_job_metadata(
|
|
28
|
+
self, offset: int, limit: int | None
|
|
29
|
+
) -> list[queued_job_t.QueuedJobMetadata]: ...
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
from sqlalchemy import JSON, BigInteger, Column, DateTime, Text
|
|
1
|
+
from sqlalchemy import JSON, BigInteger, Column, DateTime, Enum, Text
|
|
2
2
|
from sqlalchemy.orm import declarative_base
|
|
3
3
|
from sqlalchemy.sql import func
|
|
4
4
|
|
|
5
|
+
from uncountable.types import queued_job_t
|
|
6
|
+
|
|
5
7
|
Base = declarative_base()
|
|
6
8
|
|
|
7
9
|
|
|
@@ -15,3 +17,8 @@ class QueuedJob(Base):
|
|
|
15
17
|
)
|
|
16
18
|
payload = Column(JSON, nullable=False)
|
|
17
19
|
num_attempts = Column(BigInteger, nullable=False, default=0, server_default="0")
|
|
20
|
+
status = Column(
|
|
21
|
+
Enum(queued_job_t.JobStatus, length=None),
|
|
22
|
+
default=queued_job_t.JobStatus.QUEUED,
|
|
23
|
+
nullable=True,
|
|
24
|
+
)
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import sys
|
|
2
3
|
import typing
|
|
3
4
|
from concurrent.futures import ProcessPoolExecutor
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
|
|
6
7
|
from opentelemetry.trace import get_current_span
|
|
7
8
|
|
|
8
|
-
from uncountable.integration.db.connect import IntegrationDBService, create_db_engine
|
|
9
|
-
from uncountable.integration.db.session import get_session_maker
|
|
10
9
|
from uncountable.integration.queue_runner.command_server import (
|
|
11
10
|
CommandEnqueueJob,
|
|
12
11
|
CommandEnqueueJobResponse,
|
|
13
12
|
CommandQueue,
|
|
13
|
+
CommandRetryJob,
|
|
14
|
+
CommandRetryJobResponse,
|
|
14
15
|
CommandTask,
|
|
15
16
|
)
|
|
17
|
+
from uncountable.integration.queue_runner.command_server.types import (
|
|
18
|
+
CommandVaccuumQueuedJobs,
|
|
19
|
+
)
|
|
16
20
|
from uncountable.integration.queue_runner.datastore import DatastoreSqlite
|
|
17
21
|
from uncountable.integration.queue_runner.datastore.interface import Datastore
|
|
18
22
|
from uncountable.integration.queue_runner.worker import Worker
|
|
@@ -34,6 +38,10 @@ class JobListenerKey:
|
|
|
34
38
|
def _get_job_worker_key(
|
|
35
39
|
job_definition: job_definition_t.JobDefinition, profile_name: str
|
|
36
40
|
) -> JobListenerKey:
|
|
41
|
+
if job_definition.subqueue_name is not None:
|
|
42
|
+
return JobListenerKey(
|
|
43
|
+
profile_name=profile_name, subqueue_name=job_definition.subqueue_name
|
|
44
|
+
)
|
|
37
45
|
return JobListenerKey(profile_name=profile_name)
|
|
38
46
|
|
|
39
47
|
|
|
@@ -41,9 +49,12 @@ def on_worker_crash(
|
|
|
41
49
|
worker_key: JobListenerKey,
|
|
42
50
|
) -> typing.Callable[[asyncio.Task], None]:
|
|
43
51
|
def hook(task: asyncio.Task) -> None:
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
Logger(get_current_span()).log_exception(
|
|
53
|
+
Exception(
|
|
54
|
+
f"worker {worker_key.profile_name}_{worker_key.subqueue_name} crashed unexpectedly"
|
|
55
|
+
)
|
|
46
56
|
)
|
|
57
|
+
sys.exit(1)
|
|
47
58
|
|
|
48
59
|
return hook
|
|
49
60
|
|
|
@@ -75,14 +86,11 @@ def _start_workers(
|
|
|
75
86
|
return job_worker_lookup
|
|
76
87
|
|
|
77
88
|
|
|
78
|
-
async def start_scheduler(
|
|
89
|
+
async def start_scheduler(
|
|
90
|
+
command_queue: CommandQueue, datastore: DatastoreSqlite
|
|
91
|
+
) -> None:
|
|
79
92
|
logger = Logger(get_current_span())
|
|
80
93
|
result_queue: ResultQueue = asyncio.Queue()
|
|
81
|
-
engine = create_db_engine(IntegrationDBService.RUNNER)
|
|
82
|
-
session_maker = get_session_maker(engine)
|
|
83
|
-
|
|
84
|
-
datastore = DatastoreSqlite(session_maker)
|
|
85
|
-
datastore.setup(engine)
|
|
86
94
|
|
|
87
95
|
with ProcessPoolExecutor(max_workers=_MAX_JOB_WORKERS) as process_pool:
|
|
88
96
|
job_worker_lookup = _start_workers(
|
|
@@ -96,7 +104,9 @@ async def start_scheduler(command_queue: CommandQueue) -> None:
|
|
|
96
104
|
worker = job_worker_lookup[queued_job.job_ref_name]
|
|
97
105
|
except KeyError as e:
|
|
98
106
|
logger.log_exception(e)
|
|
99
|
-
datastore.
|
|
107
|
+
datastore.update_job_status(
|
|
108
|
+
queued_job.queued_job_uuid, queued_job_t.JobStatus.FAILED
|
|
109
|
+
)
|
|
100
110
|
return
|
|
101
111
|
await worker.listen_queue.put(queued_job)
|
|
102
112
|
|
|
@@ -106,19 +116,16 @@ async def start_scheduler(command_queue: CommandQueue) -> None:
|
|
|
106
116
|
) -> str:
|
|
107
117
|
if isinstance(
|
|
108
118
|
payload.invocation_context,
|
|
109
|
-
|
|
119
|
+
(
|
|
120
|
+
queued_job_t.InvocationContextCron,
|
|
121
|
+
queued_job_t.InvocationContextManual,
|
|
122
|
+
),
|
|
110
123
|
):
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
(
|
|
114
|
-
job
|
|
115
|
-
for job in existing_queued_jobs
|
|
116
|
-
if job.job_ref_name == job_ref_name
|
|
117
|
-
),
|
|
118
|
-
None,
|
|
124
|
+
existing_queued_job = datastore.get_next_queued_job_for_ref_name(
|
|
125
|
+
job_ref_name=job_ref_name
|
|
119
126
|
)
|
|
120
|
-
if
|
|
121
|
-
return
|
|
127
|
+
if existing_queued_job is not None:
|
|
128
|
+
return existing_queued_job.queued_job_uuid
|
|
122
129
|
queued_job = datastore.add_job_to_queue(
|
|
123
130
|
job_payload=payload,
|
|
124
131
|
job_ref_name=job_ref_name,
|
|
@@ -135,6 +142,25 @@ async def start_scheduler(command_queue: CommandQueue) -> None:
|
|
|
135
142
|
CommandEnqueueJobResponse(queued_job_uuid=queued_job_uuid)
|
|
136
143
|
)
|
|
137
144
|
|
|
145
|
+
async def _handle_retry_job_command(command: CommandRetryJob) -> None:
|
|
146
|
+
queued_job = datastore.retry_job(command.queued_job_uuid)
|
|
147
|
+
if queued_job is None:
|
|
148
|
+
await command.response_queue.put(
|
|
149
|
+
CommandRetryJobResponse(queued_job_uuid=None)
|
|
150
|
+
)
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
await enqueue_queued_job(queued_job)
|
|
154
|
+
await command.response_queue.put(
|
|
155
|
+
CommandRetryJobResponse(queued_job_uuid=queued_job.queued_job_uuid)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def _handle_vaccuum_queued_jobs_command(
|
|
159
|
+
command: CommandVaccuumQueuedJobs,
|
|
160
|
+
) -> None:
|
|
161
|
+
logger.log_info("Vaccuuming queued jobs...")
|
|
162
|
+
datastore.vaccuum_queued_jobs()
|
|
163
|
+
|
|
138
164
|
for queued_job in queued_jobs:
|
|
139
165
|
await enqueue_queued_job(queued_job)
|
|
140
166
|
|
|
@@ -151,10 +177,24 @@ async def start_scheduler(command_queue: CommandQueue) -> None:
|
|
|
151
177
|
match command:
|
|
152
178
|
case CommandEnqueueJob():
|
|
153
179
|
await _handle_enqueue_job_command(command=command)
|
|
180
|
+
case CommandRetryJob():
|
|
181
|
+
await _handle_retry_job_command(command=command)
|
|
182
|
+
case CommandVaccuumQueuedJobs():
|
|
183
|
+
_handle_vaccuum_queued_jobs_command(command=command)
|
|
154
184
|
case _:
|
|
155
185
|
typing.assert_never(command)
|
|
156
186
|
command_task = asyncio.create_task(command_queue.get())
|
|
157
187
|
elif task == result_task:
|
|
158
188
|
queued_job_result = result_task.result()
|
|
159
|
-
|
|
189
|
+
match queued_job_result.job_result.success:
|
|
190
|
+
case True:
|
|
191
|
+
datastore.update_job_status(
|
|
192
|
+
queued_job_result.queued_job_uuid,
|
|
193
|
+
queued_job_t.JobStatus.SUCCESS,
|
|
194
|
+
)
|
|
195
|
+
case False:
|
|
196
|
+
datastore.update_job_status(
|
|
197
|
+
queued_job_result.queued_job_uuid,
|
|
198
|
+
queued_job_t.JobStatus.FAILED,
|
|
199
|
+
)
|
|
160
200
|
result_task = asyncio.create_task(result_queue.get())
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
|
+
from uncountable.integration.db.connect import IntegrationDBService, create_db_engine
|
|
4
|
+
from uncountable.integration.db.session import get_session_maker
|
|
3
5
|
from uncountable.integration.queue_runner.command_server import serve
|
|
4
6
|
from uncountable.integration.queue_runner.command_server.types import CommandQueue
|
|
7
|
+
from uncountable.integration.queue_runner.datastore import DatastoreSqlite
|
|
5
8
|
from uncountable.integration.queue_runner.job_scheduler import start_scheduler
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
async def queue_runner_loop() -> None:
|
|
9
12
|
command_queue: CommandQueue = asyncio.Queue()
|
|
13
|
+
engine = create_db_engine(IntegrationDBService.RUNNER)
|
|
14
|
+
session_maker = get_session_maker(engine)
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
datastore = DatastoreSqlite(session_maker)
|
|
17
|
+
datastore.setup(engine)
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
command_server = asyncio.create_task(serve(command_queue, datastore))
|
|
20
|
+
|
|
21
|
+
scheduler = asyncio.create_task(start_scheduler(command_queue, datastore))
|
|
14
22
|
|
|
15
23
|
await scheduler
|
|
16
24
|
await command_server
|
|
@@ -90,7 +90,7 @@ def run_queued_job(
|
|
|
90
90
|
)
|
|
91
91
|
try:
|
|
92
92
|
client = construct_uncountable_client(
|
|
93
|
-
profile_meta=job_details.profile_metadata,
|
|
93
|
+
profile_meta=job_details.profile_metadata, logger=job_logger
|
|
94
94
|
)
|
|
95
95
|
batch_processor = AsyncBatchProcessor(client=client)
|
|
96
96
|
|
|
@@ -103,6 +103,7 @@ def run_queued_job(
|
|
|
103
103
|
profile_metadata=job_details.profile_metadata,
|
|
104
104
|
logger=job_logger,
|
|
105
105
|
payload=payload,
|
|
106
|
+
job_uuid=queued_job.queued_job_uuid,
|
|
106
107
|
)
|
|
107
108
|
|
|
108
109
|
return execute_job(
|
|
@@ -110,9 +111,6 @@ def run_queued_job(
|
|
|
110
111
|
profile_metadata=job_details.profile_metadata,
|
|
111
112
|
job_definition=job_details.job_definition,
|
|
112
113
|
)
|
|
113
|
-
except Exception as e:
|
|
114
|
-
job_logger.log_exception(e)
|
|
115
|
-
return job_definition_t.JobResult(success=False)
|
|
116
114
|
except BaseException as e:
|
|
117
115
|
job_logger.log_exception(e)
|
|
118
|
-
|
|
116
|
+
return job_definition_t.JobResult(success=False)
|
|
@@ -26,7 +26,7 @@ def load_profiles() -> list[job_definition_t.ProfileMetadata]:
|
|
|
26
26
|
profile_name = profile_file.name
|
|
27
27
|
try:
|
|
28
28
|
definition = profile_parser.parse_yaml_resource(
|
|
29
|
-
package="."
|
|
29
|
+
package=f"{profiles_module}.{profile_name}",
|
|
30
30
|
resource="profile.yaml",
|
|
31
31
|
)
|
|
32
32
|
for job in definition.jobs:
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import datetime
|
|
1
2
|
import multiprocessing
|
|
2
3
|
import subprocess
|
|
3
4
|
import sys
|
|
4
5
|
import time
|
|
5
6
|
from dataclasses import dataclass
|
|
6
|
-
from datetime import
|
|
7
|
+
from datetime import UTC
|
|
8
|
+
from enum import StrEnum
|
|
7
9
|
|
|
8
10
|
from opentelemetry.trace import get_current_span
|
|
9
11
|
|
|
@@ -18,11 +20,19 @@ from uncountable.integration.telemetry import Logger
|
|
|
18
20
|
|
|
19
21
|
SHUTDOWN_TIMEOUT_SECS = 30
|
|
20
22
|
|
|
23
|
+
AnyProcess = multiprocessing.Process | subprocess.Popen[bytes]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProcessName(StrEnum):
|
|
27
|
+
QUEUE_RUNNER = "queue_runner"
|
|
28
|
+
CRON_SERVER = "cron_server"
|
|
29
|
+
UWSGI = "uwsgi"
|
|
30
|
+
|
|
21
31
|
|
|
22
32
|
@dataclass(kw_only=True)
|
|
23
33
|
class ProcessInfo:
|
|
24
|
-
name:
|
|
25
|
-
process:
|
|
34
|
+
name: ProcessName
|
|
35
|
+
process: AnyProcess
|
|
26
36
|
|
|
27
37
|
@property
|
|
28
38
|
def is_alive(self) -> bool:
|
|
@@ -45,14 +55,14 @@ class ProcessInfo:
|
|
|
45
55
|
return self.process.poll()
|
|
46
56
|
|
|
47
57
|
|
|
48
|
-
def handle_shutdown(logger: Logger, processes:
|
|
58
|
+
def handle_shutdown(logger: Logger, processes: dict[ProcessName, ProcessInfo]) -> None:
|
|
49
59
|
logger.log_info("received shutdown command, shutting down sub-processes")
|
|
50
|
-
for proc_info in processes:
|
|
60
|
+
for proc_info in processes.values():
|
|
51
61
|
if proc_info.is_alive:
|
|
52
62
|
proc_info.process.terminate()
|
|
53
63
|
|
|
54
64
|
shutdown_start = time.time()
|
|
55
|
-
still_living_processes = processes
|
|
65
|
+
still_living_processes = list(processes.values())
|
|
56
66
|
while (
|
|
57
67
|
time.time() - shutdown_start < SHUTDOWN_TIMEOUT_SECS
|
|
58
68
|
and len(still_living_processes) > 0
|
|
@@ -81,46 +91,84 @@ def handle_shutdown(logger: Logger, processes: list[ProcessInfo]) -> None:
|
|
|
81
91
|
proc_info.process.kill()
|
|
82
92
|
|
|
83
93
|
|
|
84
|
-
def
|
|
85
|
-
|
|
94
|
+
def restart_process(
|
|
95
|
+
logger: Logger, proc_info: ProcessInfo, processes: dict[ProcessName, ProcessInfo]
|
|
96
|
+
) -> None:
|
|
97
|
+
logger.log_error(
|
|
98
|
+
f"process {proc_info.name} shut down unexpectedly - exit code {proc_info.exitcode}. Restarting..."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
match proc_info.name:
|
|
102
|
+
case ProcessName.QUEUE_RUNNER:
|
|
103
|
+
queue_proc = multiprocessing.Process(target=start_queue_runner)
|
|
104
|
+
queue_proc.start()
|
|
105
|
+
new_info = ProcessInfo(name=ProcessName.QUEUE_RUNNER, process=queue_proc)
|
|
106
|
+
processes[ProcessName.QUEUE_RUNNER] = new_info
|
|
107
|
+
try:
|
|
108
|
+
_wait_queue_runner_online()
|
|
109
|
+
logger.log_info("queue runner restarted successfully")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.log_exception(e)
|
|
112
|
+
logger.log_error(
|
|
113
|
+
"queue runner failed to restart, shutting down scheduler"
|
|
114
|
+
)
|
|
115
|
+
handle_shutdown(logger, processes)
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
|
|
118
|
+
case ProcessName.CRON_SERVER:
|
|
119
|
+
cron_proc = multiprocessing.Process(target=cron_target)
|
|
120
|
+
cron_proc.start()
|
|
121
|
+
new_info = ProcessInfo(name=ProcessName.CRON_SERVER, process=cron_proc)
|
|
122
|
+
processes[ProcessName.CRON_SERVER] = new_info
|
|
123
|
+
logger.log_info("cron server restarted successfully")
|
|
124
|
+
|
|
125
|
+
case ProcessName.UWSGI:
|
|
126
|
+
uwsgi_proc: AnyProcess = subprocess.Popen(["uwsgi", "--die-on-term"])
|
|
127
|
+
new_info = ProcessInfo(name=ProcessName.UWSGI, process=uwsgi_proc)
|
|
128
|
+
processes[ProcessName.UWSGI] = new_info
|
|
129
|
+
logger.log_info("uwsgi restarted successfully")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def check_process_alive(
|
|
133
|
+
logger: Logger, processes: dict[ProcessName, ProcessInfo]
|
|
134
|
+
) -> None:
|
|
135
|
+
for proc_info in processes.values():
|
|
86
136
|
if not proc_info.is_alive:
|
|
87
|
-
logger
|
|
88
|
-
f"process {proc_info.name} shut down unexpectedly! shutting down scheduler; exit code is {proc_info.exitcode}"
|
|
89
|
-
)
|
|
90
|
-
handle_shutdown(logger, processes)
|
|
91
|
-
sys.exit(1)
|
|
137
|
+
restart_process(logger, proc_info, processes)
|
|
92
138
|
|
|
93
139
|
|
|
94
140
|
def _wait_queue_runner_online() -> None:
|
|
95
|
-
|
|
96
|
-
|
|
141
|
+
MAX_QUEUE_RUNNER_HEALTH_CHECKS = 10
|
|
142
|
+
QUEUE_RUNNER_HEALTH_CHECK_DELAY_SECS = 1
|
|
97
143
|
|
|
98
144
|
num_attempts = 0
|
|
99
|
-
before = datetime.now(
|
|
100
|
-
while num_attempts <
|
|
145
|
+
before = datetime.datetime.now(UTC)
|
|
146
|
+
while num_attempts < MAX_QUEUE_RUNNER_HEALTH_CHECKS:
|
|
101
147
|
try:
|
|
102
148
|
if check_health(port=get_local_admin_server_port()):
|
|
103
149
|
return
|
|
104
150
|
except CommandServerTimeout:
|
|
105
151
|
pass
|
|
106
152
|
num_attempts += 1
|
|
107
|
-
time.sleep(
|
|
108
|
-
after = datetime.now(
|
|
153
|
+
time.sleep(QUEUE_RUNNER_HEALTH_CHECK_DELAY_SECS)
|
|
154
|
+
after = datetime.datetime.now(UTC)
|
|
109
155
|
duration_secs = (after - before).seconds
|
|
110
156
|
raise Exception(f"queue runner failed to come online after {duration_secs} seconds")
|
|
111
157
|
|
|
112
158
|
|
|
113
159
|
def main() -> None:
|
|
114
160
|
logger = Logger(get_current_span())
|
|
115
|
-
processes:
|
|
161
|
+
processes: dict[ProcessName, ProcessInfo] = {}
|
|
162
|
+
|
|
163
|
+
multiprocessing.set_start_method("forkserver")
|
|
116
164
|
|
|
117
165
|
def add_process(process: ProcessInfo) -> None:
|
|
118
|
-
processes.
|
|
166
|
+
processes[process.name] = process
|
|
119
167
|
logger.log_info(f"started process {process.name}")
|
|
120
168
|
|
|
121
169
|
runner_process = multiprocessing.Process(target=start_queue_runner)
|
|
122
170
|
runner_process.start()
|
|
123
|
-
add_process(ProcessInfo(name=
|
|
171
|
+
add_process(ProcessInfo(name=ProcessName.QUEUE_RUNNER, process=runner_process))
|
|
124
172
|
|
|
125
173
|
try:
|
|
126
174
|
_wait_queue_runner_online()
|
|
@@ -131,13 +179,13 @@ def main() -> None:
|
|
|
131
179
|
|
|
132
180
|
cron_process = multiprocessing.Process(target=cron_target)
|
|
133
181
|
cron_process.start()
|
|
134
|
-
add_process(ProcessInfo(name=
|
|
182
|
+
add_process(ProcessInfo(name=ProcessName.CRON_SERVER, process=cron_process))
|
|
135
183
|
|
|
136
184
|
uwsgi_process = subprocess.Popen([
|
|
137
185
|
"uwsgi",
|
|
138
186
|
"--die-on-term",
|
|
139
187
|
])
|
|
140
|
-
add_process(ProcessInfo(name=
|
|
188
|
+
add_process(ProcessInfo(name=ProcessName.UWSGI, process=uwsgi_process))
|
|
141
189
|
|
|
142
190
|
try:
|
|
143
191
|
while True:
|
|
@@ -147,4 +195,5 @@ def main() -> None:
|
|
|
147
195
|
handle_shutdown(logger, processes=processes)
|
|
148
196
|
|
|
149
197
|
|
|
150
|
-
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
main()
|
|
@@ -54,7 +54,7 @@ def _load_secret_overrides(profile_name: str) -> dict[SecretRetrieval, str]:
|
|
|
54
54
|
profiles_module = os.environ["UNC_PROFILES_MODULE"]
|
|
55
55
|
try:
|
|
56
56
|
overrides = overrides_parser.parse_yaml_resource(
|
|
57
|
-
package="."
|
|
57
|
+
package=f"{profiles_module}.{profile_name}",
|
|
58
58
|
resource="local_overrides.yaml",
|
|
59
59
|
)
|
|
60
60
|
return {
|