UncountablePythonSDK 0.0.52__py3-none-any.whl → 0.0.131__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/async_batch.py +3 -3
- examples/basic_auth.py +7 -0
- examples/create_entity.py +3 -1
- examples/create_ingredient_sdk.py +34 -0
- examples/download_files.py +26 -0
- examples/edit_recipe_inputs.py +4 -2
- examples/integration-server/jobs/materials_auto/concurrent_cron.py +11 -0
- examples/integration-server/jobs/materials_auto/example_cron.py +21 -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 +23 -0
- examples/integration-server/jobs/materials_auto/profile.yaml +104 -0
- examples/integration-server/pyproject.toml +224 -0
- examples/invoke_uploader.py +4 -1
- examples/oauth.py +7 -0
- examples/set_recipe_metadata_file.py +40 -0
- examples/set_recipe_output_file_sdk.py +26 -0
- examples/upload_files.py +1 -2
- pkgs/argument_parser/__init__.py +9 -0
- pkgs/argument_parser/_is_namedtuple.py +3 -0
- pkgs/argument_parser/argument_parser.py +217 -70
- pkgs/filesystem_utils/__init__.py +1 -0
- pkgs/filesystem_utils/_blob_session.py +144 -0
- pkgs/filesystem_utils/_gdrive_session.py +10 -7
- pkgs/filesystem_utils/_s3_session.py +15 -13
- pkgs/filesystem_utils/_sftp_session.py +11 -7
- pkgs/filesystem_utils/file_type_utils.py +30 -10
- pkgs/py.typed +0 -0
- 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 +47 -26
- pkgs/serialization/serial_generic.py +16 -0
- pkgs/serialization/serial_union.py +17 -14
- pkgs/serialization/yaml.py +4 -1
- 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 +5 -5
- pkgs/type_spec/builder.py +354 -119
- pkgs/type_spec/builder_types.py +9 -0
- pkgs/type_spec/config.py +51 -11
- pkgs/type_spec/cross_output_links.py +99 -0
- pkgs/type_spec/emit_io_ts.py +1 -1
- pkgs/type_spec/emit_open_api.py +127 -36
- pkgs/type_spec/emit_open_api_util.py +5 -6
- pkgs/type_spec/emit_python.py +329 -121
- pkgs/type_spec/emit_typescript.py +117 -256
- pkgs/type_spec/emit_typescript_util.py +291 -2
- pkgs/type_spec/load_types.py +18 -4
- 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 +13 -10
- 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 +124 -29
- 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 +4 -4
- pkgs/type_spec/value_spec/__main__.py +26 -9
- pkgs/type_spec/value_spec/convert_type.py +21 -1
- pkgs/type_spec/value_spec/emit_python.py +25 -7
- pkgs/type_spec/value_spec/types.py +1 -1
- uncountable/core/async_batch.py +1 -1
- uncountable/core/client.py +142 -39
- uncountable/core/environment.py +41 -0
- uncountable/core/file_upload.py +52 -18
- uncountable/integration/cli.py +142 -0
- uncountable/integration/construct_client.py +8 -8
- uncountable/integration/cron.py +11 -37
- uncountable/integration/db/connect.py +12 -2
- uncountable/integration/db/session.py +25 -0
- uncountable/integration/entrypoint.py +8 -37
- uncountable/integration/executors/executors.py +125 -2
- uncountable/integration/executors/generic_upload_executor.py +87 -29
- uncountable/integration/executors/script_executor.py +3 -3
- uncountable/integration/http_server/__init__.py +5 -0
- uncountable/integration/http_server/types.py +69 -0
- uncountable/integration/job.py +242 -12
- uncountable/integration/queue_runner/__init__.py +0 -0
- uncountable/integration/queue_runner/command_server/__init__.py +28 -0
- uncountable/integration/queue_runner/command_server/command_client.py +133 -0
- uncountable/integration/queue_runner/command_server/command_server.py +142 -0
- uncountable/integration/queue_runner/command_server/constants.py +4 -0
- uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server.proto +58 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +57 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +114 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +264 -0
- uncountable/integration/queue_runner/command_server/types.py +75 -0
- uncountable/integration/queue_runner/datastore/__init__.py +3 -0
- uncountable/integration/queue_runner/datastore/datastore_sqlite.py +250 -0
- uncountable/integration/queue_runner/datastore/interface.py +29 -0
- uncountable/integration/queue_runner/datastore/model.py +24 -0
- uncountable/integration/queue_runner/job_scheduler.py +200 -0
- uncountable/integration/queue_runner/queue_runner.py +34 -0
- uncountable/integration/queue_runner/types.py +7 -0
- uncountable/integration/queue_runner/worker.py +116 -0
- uncountable/integration/scan_profiles.py +67 -0
- uncountable/integration/scheduler.py +199 -0
- uncountable/integration/secret_retrieval/retrieve_secret.py +26 -4
- uncountable/integration/server.py +94 -69
- uncountable/integration/telemetry.py +150 -34
- uncountable/integration/webhook_server/entrypoint.py +97 -0
- uncountable/types/__init__.py +78 -1
- uncountable/types/api/batch/execute_batch.py +13 -6
- uncountable/types/api/batch/execute_batch_load_async.py +9 -3
- uncountable/types/api/chemical/convert_chemical_formats.py +17 -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 +19 -7
- uncountable/types/api/entity/create_entity.py +17 -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 +13 -4
- uncountable/types/api/entity/grant_entity_permissions.py +48 -0
- uncountable/types/api/entity/list_aggregate.py +79 -0
- uncountable/types/api/entity/list_entities.py +42 -10
- uncountable/types/api/entity/lock_entity.py +11 -4
- uncountable/types/api/entity/lookup_entity.py +116 -0
- uncountable/types/api/entity/resolve_entity_ids.py +15 -6
- uncountable/types/api/entity/set_entity_field_values.py +44 -0
- uncountable/types/api/entity/set_values.py +10 -3
- uncountable/types/api/entity/transition_entity_phase.py +22 -7
- uncountable/types/api/entity/unlock_entity.py +10 -3
- uncountable/types/api/equipment/associate_equipment_input.py +9 -3
- uncountable/types/api/field_options/upsert_field_options.py +17 -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 +16 -7
- uncountable/types/api/id_source/match_id_source.py +14 -5
- uncountable/types/api/input_groups/get_input_group_names.py +13 -4
- uncountable/types/api/inputs/create_inputs.py +23 -9
- uncountable/types/api/inputs/get_input_data.py +30 -12
- uncountable/types/api/inputs/get_input_names.py +16 -7
- uncountable/types/api/inputs/get_inputs_data.py +25 -7
- uncountable/types/api/inputs/set_input_attribute_values.py +12 -6
- uncountable/types/api/inputs/set_input_category.py +12 -5
- uncountable/types/api/inputs/set_input_subcategories.py +10 -3
- uncountable/types/api/inputs/set_intermediate_type.py +11 -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 +10 -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 +28 -13
- uncountable/types/api/outputs/get_output_names.py +15 -6
- uncountable/types/api/outputs/get_output_organization.py +173 -0
- uncountable/types/api/outputs/resolve_output_conditions.py +20 -8
- uncountable/types/api/permissions/set_core_permissions.py +26 -10
- uncountable/types/api/project/get_projects.py +16 -7
- uncountable/types/api/project/get_projects_data.py +17 -8
- uncountable/types/api/recipe_links/create_recipe_link.py +12 -5
- uncountable/types/api/recipe_links/remove_recipe_link.py +11 -4
- uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +16 -7
- uncountable/types/api/recipes/add_recipe_to_project.py +10 -3
- uncountable/types/api/recipes/add_time_series_data.py +64 -0
- uncountable/types/api/recipes/archive_recipes.py +11 -4
- uncountable/types/api/recipes/associate_recipe_as_input.py +12 -5
- uncountable/types/api/recipes/associate_recipe_as_lot.py +10 -3
- uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
- uncountable/types/api/recipes/create_mix_order.py +44 -0
- uncountable/types/api/recipes/create_recipe.py +15 -9
- uncountable/types/api/recipes/create_recipes.py +21 -9
- uncountable/types/api/recipes/disassociate_recipe_as_input.py +10 -3
- uncountable/types/api/recipes/edit_recipe_inputs.py +134 -22
- uncountable/types/api/recipes/get_column_calculation_values.py +57 -0
- uncountable/types/api/recipes/get_curve.py +11 -5
- uncountable/types/api/recipes/get_recipe_calculations.py +13 -7
- uncountable/types/api/recipes/get_recipe_links.py +10 -4
- uncountable/types/api/recipes/get_recipe_names.py +13 -4
- uncountable/types/api/recipes/get_recipe_output_metadata.py +12 -6
- uncountable/types/api/recipes/get_recipes_data.py +87 -33
- uncountable/types/api/recipes/lock_recipes.py +19 -8
- uncountable/types/api/recipes/remove_recipe_from_project.py +10 -3
- uncountable/types/api/recipes/set_recipe_inputs.py +16 -10
- uncountable/types/api/recipes/set_recipe_metadata.py +10 -3
- uncountable/types/api/recipes/set_recipe_output_annotations.py +24 -12
- uncountable/types/api/recipes/set_recipe_output_file.py +55 -0
- uncountable/types/api/recipes/set_recipe_outputs.py +35 -12
- uncountable/types/api/recipes/set_recipe_tags.py +26 -9
- uncountable/types/api/recipes/set_recipe_total.py +59 -0
- uncountable/types/api/recipes/unarchive_recipes.py +10 -3
- uncountable/types/api/recipes/unlock_recipes.py +14 -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 +11 -4
- uncountable/types/api/uploader/complete_async_parse.py +46 -0
- uncountable/types/api/uploader/invoke_uploader.py +13 -6
- uncountable/types/api/user/__init__.py +1 -0
- uncountable/types/api/user/get_current_user_info.py +40 -0
- uncountable/types/async_batch.py +2 -1
- uncountable/types/async_batch_processor.py +618 -18
- uncountable/types/async_batch_t.py +54 -7
- uncountable/types/async_jobs.py +8 -0
- uncountable/types/async_jobs_t.py +52 -0
- uncountable/types/auth_retrieval.py +11 -0
- uncountable/types/auth_retrieval_t.py +75 -0
- uncountable/types/base.py +0 -1
- uncountable/types/base_t.py +13 -11
- uncountable/types/calculations.py +0 -1
- uncountable/types/calculations_t.py +5 -2
- uncountable/types/chemical_structure.py +0 -1
- uncountable/types/chemical_structure_t.py +6 -5
- uncountable/types/client_base.py +751 -70
- uncountable/types/client_config.py +1 -1
- uncountable/types/client_config_t.py +17 -3
- uncountable/types/curves.py +0 -1
- uncountable/types/curves_t.py +10 -7
- uncountable/types/data.py +12 -0
- uncountable/types/data_t.py +103 -0
- uncountable/types/entity.py +4 -1
- uncountable/types/entity_t.py +125 -7
- uncountable/types/experiment_groups.py +0 -1
- uncountable/types/experiment_groups_t.py +5 -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 +246 -9
- uncountable/types/fields.py +0 -1
- uncountable/types/fields_t.py +5 -2
- uncountable/types/generic_upload.py +6 -1
- uncountable/types/generic_upload_t.py +88 -9
- uncountable/types/id_source.py +0 -1
- uncountable/types/id_source_t.py +26 -7
- uncountable/types/identifier.py +0 -1
- uncountable/types/identifier_t.py +13 -5
- uncountable/types/input_attributes.py +0 -1
- uncountable/types/input_attributes_t.py +4 -4
- uncountable/types/inputs.py +1 -1
- uncountable/types/inputs_t.py +24 -4
- uncountable/types/integration_server.py +8 -0
- uncountable/types/integration_server_t.py +46 -0
- 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 +4 -6
- uncountable/types/job_definition_t.py +96 -65
- 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 +6 -3
- uncountable/types/overrides.py +9 -0
- uncountable/types/overrides_t.py +49 -0
- uncountable/types/permissions.py +0 -1
- uncountable/types/permissions_t.py +1 -2
- uncountable/types/phases.py +0 -1
- uncountable/types/phases_t.py +5 -2
- uncountable/types/post_base.py +0 -1
- uncountable/types/post_base_t.py +1 -2
- uncountable/types/queued_job.py +17 -0
- uncountable/types/queued_job_t.py +140 -0
- uncountable/types/recipe_identifiers.py +0 -1
- uncountable/types/recipe_identifiers_t.py +21 -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 +7 -4
- uncountable/types/recipe_metadata.py +0 -1
- uncountable/types/recipe_metadata_t.py +14 -9
- uncountable/types/recipe_output_metadata.py +0 -1
- uncountable/types/recipe_output_metadata_t.py +5 -2
- uncountable/types/recipe_tags.py +0 -1
- uncountable/types/recipe_tags_t.py +5 -2
- uncountable/types/recipe_workflow_steps.py +0 -1
- uncountable/types/recipe_workflow_steps_t.py +14 -7
- uncountable/types/recipes.py +0 -1
- uncountable/types/recipes_t.py +6 -2
- uncountable/types/response.py +0 -1
- uncountable/types/response_t.py +3 -2
- uncountable/types/secret_retrieval.py +0 -1
- uncountable/types/secret_retrieval_t.py +13 -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 +5 -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 +5 -2
- uncountable/types/webhook_job.py +9 -0
- uncountable/types/webhook_job_t.py +48 -0
- uncountable/types/workflows.py +0 -1
- uncountable/types/workflows_t.py +10 -4
- uncountablepythonsdk-0.0.131.dist-info/METADATA +64 -0
- uncountablepythonsdk-0.0.131.dist-info/RECORD +363 -0
- {UncountablePythonSDK-0.0.52.dist-info → uncountablepythonsdk-0.0.131.dist-info}/WHEEL +1 -1
- UncountablePythonSDK-0.0.52.dist-info/METADATA +0 -56
- UncountablePythonSDK-0.0.52.dist-info/RECORD +0 -246
- docs/quickstart.md +0 -19
- uncountable/core/version.py +0 -11
- {UncountablePythonSDK-0.0.52.dist-info → uncountablepythonsdk-0.0.131.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
import typing
|
|
4
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from opentelemetry.trace import get_current_span
|
|
8
|
+
|
|
9
|
+
from uncountable.integration.queue_runner.command_server import (
|
|
10
|
+
CommandEnqueueJob,
|
|
11
|
+
CommandEnqueueJobResponse,
|
|
12
|
+
CommandQueue,
|
|
13
|
+
CommandRetryJob,
|
|
14
|
+
CommandRetryJobResponse,
|
|
15
|
+
CommandTask,
|
|
16
|
+
)
|
|
17
|
+
from uncountable.integration.queue_runner.command_server.types import (
|
|
18
|
+
CommandVaccuumQueuedJobs,
|
|
19
|
+
)
|
|
20
|
+
from uncountable.integration.queue_runner.datastore import DatastoreSqlite
|
|
21
|
+
from uncountable.integration.queue_runner.datastore.interface import Datastore
|
|
22
|
+
from uncountable.integration.queue_runner.worker import Worker
|
|
23
|
+
from uncountable.integration.scan_profiles import load_profiles
|
|
24
|
+
from uncountable.integration.telemetry import Logger
|
|
25
|
+
from uncountable.types import job_definition_t, queued_job_t
|
|
26
|
+
|
|
27
|
+
from .types import ResultQueue, ResultTask
|
|
28
|
+
|
|
29
|
+
_MAX_JOB_WORKERS = 5
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(kw_only=True, frozen=True)
|
|
33
|
+
class JobListenerKey:
|
|
34
|
+
profile_name: str
|
|
35
|
+
subqueue_name: str = "default"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_job_worker_key(
|
|
39
|
+
job_definition: job_definition_t.JobDefinition, profile_name: str
|
|
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
|
+
)
|
|
45
|
+
return JobListenerKey(profile_name=profile_name)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def on_worker_crash(
|
|
49
|
+
worker_key: JobListenerKey,
|
|
50
|
+
) -> typing.Callable[[asyncio.Task], None]:
|
|
51
|
+
def hook(task: asyncio.Task) -> None:
|
|
52
|
+
Logger(get_current_span()).log_exception(
|
|
53
|
+
Exception(
|
|
54
|
+
f"worker {worker_key.profile_name}_{worker_key.subqueue_name} crashed unexpectedly"
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
return hook
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _start_workers(
|
|
63
|
+
process_pool: ProcessPoolExecutor, result_queue: ResultQueue, datastore: Datastore
|
|
64
|
+
) -> dict[str, Worker]:
|
|
65
|
+
profiles = load_profiles()
|
|
66
|
+
job_queue_worker_lookup: dict[JobListenerKey, Worker] = {}
|
|
67
|
+
job_worker_lookup: dict[str, Worker] = {}
|
|
68
|
+
job_definition_lookup: dict[str, job_definition_t.JobDefinition] = {}
|
|
69
|
+
for profile in profiles:
|
|
70
|
+
for job_definition in profile.jobs:
|
|
71
|
+
job_definition_lookup[job_definition.id] = job_definition
|
|
72
|
+
job_worker_key = _get_job_worker_key(job_definition, profile.name)
|
|
73
|
+
if job_worker_key not in job_queue_worker_lookup:
|
|
74
|
+
worker = Worker(
|
|
75
|
+
process_pool=process_pool,
|
|
76
|
+
listen_queue=asyncio.Queue(),
|
|
77
|
+
result_queue=result_queue,
|
|
78
|
+
datastore=datastore,
|
|
79
|
+
)
|
|
80
|
+
task = asyncio.create_task(worker.run_worker_loop())
|
|
81
|
+
task.add_done_callback(on_worker_crash(job_worker_key))
|
|
82
|
+
job_queue_worker_lookup[job_worker_key] = worker
|
|
83
|
+
job_worker_lookup[job_definition.id] = job_queue_worker_lookup[
|
|
84
|
+
job_worker_key
|
|
85
|
+
]
|
|
86
|
+
return job_worker_lookup
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def start_scheduler(
|
|
90
|
+
command_queue: CommandQueue, datastore: DatastoreSqlite
|
|
91
|
+
) -> None:
|
|
92
|
+
logger = Logger(get_current_span())
|
|
93
|
+
result_queue: ResultQueue = asyncio.Queue()
|
|
94
|
+
|
|
95
|
+
with ProcessPoolExecutor(max_workers=_MAX_JOB_WORKERS) as process_pool:
|
|
96
|
+
job_worker_lookup = _start_workers(
|
|
97
|
+
process_pool, result_queue, datastore=datastore
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
queued_jobs = datastore.load_job_queue()
|
|
101
|
+
|
|
102
|
+
async def enqueue_queued_job(queued_job: queued_job_t.QueuedJob) -> None:
|
|
103
|
+
try:
|
|
104
|
+
worker = job_worker_lookup[queued_job.job_ref_name]
|
|
105
|
+
except KeyError as e:
|
|
106
|
+
logger.log_exception(e)
|
|
107
|
+
datastore.update_job_status(
|
|
108
|
+
queued_job.queued_job_uuid, queued_job_t.JobStatus.FAILED
|
|
109
|
+
)
|
|
110
|
+
return
|
|
111
|
+
await worker.listen_queue.put(queued_job)
|
|
112
|
+
|
|
113
|
+
async def _enqueue_or_deduplicate_job(
|
|
114
|
+
job_ref_name: str,
|
|
115
|
+
payload: queued_job_t.QueuedJobPayload,
|
|
116
|
+
) -> str:
|
|
117
|
+
if isinstance(
|
|
118
|
+
payload.invocation_context,
|
|
119
|
+
(
|
|
120
|
+
queued_job_t.InvocationContextCron,
|
|
121
|
+
queued_job_t.InvocationContextManual,
|
|
122
|
+
),
|
|
123
|
+
):
|
|
124
|
+
existing_queued_job = datastore.get_next_queued_job_for_ref_name(
|
|
125
|
+
job_ref_name=job_ref_name
|
|
126
|
+
)
|
|
127
|
+
if existing_queued_job is not None:
|
|
128
|
+
return existing_queued_job.queued_job_uuid
|
|
129
|
+
queued_job = datastore.add_job_to_queue(
|
|
130
|
+
job_payload=payload,
|
|
131
|
+
job_ref_name=job_ref_name,
|
|
132
|
+
)
|
|
133
|
+
await enqueue_queued_job(queued_job)
|
|
134
|
+
return queued_job.queued_job_uuid
|
|
135
|
+
|
|
136
|
+
async def _handle_enqueue_job_command(command: CommandEnqueueJob) -> None:
|
|
137
|
+
queued_job_uuid = await _enqueue_or_deduplicate_job(
|
|
138
|
+
job_ref_name=command.job_ref_name,
|
|
139
|
+
payload=command.payload,
|
|
140
|
+
)
|
|
141
|
+
await command.response_queue.put(
|
|
142
|
+
CommandEnqueueJobResponse(queued_job_uuid=queued_job_uuid)
|
|
143
|
+
)
|
|
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
|
+
|
|
164
|
+
for queued_job in queued_jobs:
|
|
165
|
+
await enqueue_queued_job(queued_job)
|
|
166
|
+
|
|
167
|
+
result_task: ResultTask = asyncio.create_task(result_queue.get())
|
|
168
|
+
command_task: CommandTask = asyncio.create_task(command_queue.get())
|
|
169
|
+
while True:
|
|
170
|
+
finished, _ = await asyncio.wait(
|
|
171
|
+
[result_task, command_task], return_when=asyncio.FIRST_COMPLETED
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
for task in finished:
|
|
175
|
+
if task == command_task:
|
|
176
|
+
command = command_task.result()
|
|
177
|
+
match command:
|
|
178
|
+
case CommandEnqueueJob():
|
|
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)
|
|
184
|
+
case _:
|
|
185
|
+
typing.assert_never(command)
|
|
186
|
+
command_task = asyncio.create_task(command_queue.get())
|
|
187
|
+
elif task == result_task:
|
|
188
|
+
queued_job_result = result_task.result()
|
|
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
|
+
)
|
|
200
|
+
result_task = asyncio.create_task(result_queue.get())
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from uncountable.integration.db.connect import IntegrationDBService, create_db_engine
|
|
4
|
+
from uncountable.integration.db.session import get_session_maker
|
|
5
|
+
from uncountable.integration.queue_runner.command_server import serve
|
|
6
|
+
from uncountable.integration.queue_runner.command_server.types import CommandQueue
|
|
7
|
+
from uncountable.integration.queue_runner.datastore import DatastoreSqlite
|
|
8
|
+
from uncountable.integration.queue_runner.job_scheduler import start_scheduler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def queue_runner_loop() -> None:
|
|
12
|
+
command_queue: CommandQueue = asyncio.Queue()
|
|
13
|
+
engine = create_db_engine(IntegrationDBService.RUNNER)
|
|
14
|
+
session_maker = get_session_maker(engine)
|
|
15
|
+
|
|
16
|
+
datastore = DatastoreSqlite(session_maker)
|
|
17
|
+
datastore.setup(engine)
|
|
18
|
+
|
|
19
|
+
command_server = asyncio.create_task(serve(command_queue, datastore))
|
|
20
|
+
|
|
21
|
+
scheduler = asyncio.create_task(start_scheduler(command_queue, datastore))
|
|
22
|
+
|
|
23
|
+
await scheduler
|
|
24
|
+
await command_server
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def start_queue_runner() -> None:
|
|
28
|
+
loop = asyncio.new_event_loop()
|
|
29
|
+
loop.run_until_complete(queue_runner_loop())
|
|
30
|
+
loop.close()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
start_queue_runner()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from opentelemetry.trace import get_current_span
|
|
6
|
+
|
|
7
|
+
from uncountable.core.async_batch import AsyncBatchProcessor
|
|
8
|
+
from uncountable.integration.construct_client import construct_uncountable_client
|
|
9
|
+
from uncountable.integration.executors.executors import execute_job
|
|
10
|
+
from uncountable.integration.job import JobArguments
|
|
11
|
+
from uncountable.integration.queue_runner.datastore.interface import Datastore
|
|
12
|
+
from uncountable.integration.queue_runner.types import ListenQueue, ResultQueue
|
|
13
|
+
from uncountable.integration.scan_profiles import load_profiles
|
|
14
|
+
from uncountable.integration.telemetry import JobLogger, Logger, get_otel_tracer
|
|
15
|
+
from uncountable.types import base_t, job_definition_t, queued_job_t
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Worker:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
process_pool: ProcessPoolExecutor,
|
|
23
|
+
listen_queue: ListenQueue,
|
|
24
|
+
result_queue: ResultQueue,
|
|
25
|
+
datastore: Datastore,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.process_pool = process_pool
|
|
28
|
+
self.listen_queue = listen_queue
|
|
29
|
+
self.result_queue = result_queue
|
|
30
|
+
self.datastore = datastore
|
|
31
|
+
|
|
32
|
+
async def run_worker_loop(self) -> None:
|
|
33
|
+
logger = Logger(get_current_span())
|
|
34
|
+
while True:
|
|
35
|
+
try:
|
|
36
|
+
queued_job = await self.listen_queue.get()
|
|
37
|
+
self.datastore.increment_num_attempts(queued_job.queued_job_uuid)
|
|
38
|
+
loop = asyncio.get_event_loop()
|
|
39
|
+
result = await loop.run_in_executor(
|
|
40
|
+
self.process_pool, run_queued_job, queued_job
|
|
41
|
+
)
|
|
42
|
+
assert isinstance(result, job_definition_t.JobResult)
|
|
43
|
+
await self.result_queue.put(
|
|
44
|
+
queued_job_t.QueuedJobResult(
|
|
45
|
+
job_result=result, queued_job_uuid=queued_job.queued_job_uuid
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
except BaseException as e:
|
|
49
|
+
logger.log_exception(e)
|
|
50
|
+
raise e
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(kw_only=True)
|
|
54
|
+
class RegisteredJobDetails:
|
|
55
|
+
profile_metadata: job_definition_t.ProfileMetadata
|
|
56
|
+
job_definition: job_definition_t.JobDefinition
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_registered_job_details(job_ref_name: str) -> RegisteredJobDetails:
|
|
60
|
+
profiles = load_profiles()
|
|
61
|
+
for profile_metadata in profiles:
|
|
62
|
+
for job_definition in profile_metadata.jobs:
|
|
63
|
+
if job_definition.id == job_ref_name:
|
|
64
|
+
return RegisteredJobDetails(
|
|
65
|
+
profile_metadata=profile_metadata,
|
|
66
|
+
job_definition=job_definition,
|
|
67
|
+
)
|
|
68
|
+
raise Exception(f"profile not found for job {job_ref_name}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _resolve_queued_job_payload(queued_job: queued_job_t.QueuedJob) -> base_t.JsonValue:
|
|
72
|
+
match queued_job.payload.invocation_context:
|
|
73
|
+
case queued_job_t.InvocationContextCron():
|
|
74
|
+
return None
|
|
75
|
+
case queued_job_t.InvocationContextManual():
|
|
76
|
+
return None
|
|
77
|
+
case queued_job_t.InvocationContextWebhook():
|
|
78
|
+
return queued_job.payload.invocation_context.webhook_payload
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def run_queued_job(
|
|
82
|
+
queued_job: queued_job_t.QueuedJob,
|
|
83
|
+
) -> job_definition_t.JobResult:
|
|
84
|
+
with get_otel_tracer().start_as_current_span(name="run_queued_job") as span:
|
|
85
|
+
job_details = get_registered_job_details(queued_job.job_ref_name)
|
|
86
|
+
job_logger = JobLogger(
|
|
87
|
+
base_span=span,
|
|
88
|
+
profile_metadata=job_details.profile_metadata,
|
|
89
|
+
job_definition=job_details.job_definition,
|
|
90
|
+
)
|
|
91
|
+
try:
|
|
92
|
+
client = construct_uncountable_client(
|
|
93
|
+
profile_meta=job_details.profile_metadata, logger=job_logger
|
|
94
|
+
)
|
|
95
|
+
batch_processor = AsyncBatchProcessor(client=client)
|
|
96
|
+
|
|
97
|
+
payload = _resolve_queued_job_payload(queued_job)
|
|
98
|
+
|
|
99
|
+
args = JobArguments(
|
|
100
|
+
job_definition=job_details.job_definition,
|
|
101
|
+
client=client,
|
|
102
|
+
batch_processor=batch_processor,
|
|
103
|
+
profile_metadata=job_details.profile_metadata,
|
|
104
|
+
logger=job_logger,
|
|
105
|
+
payload=payload,
|
|
106
|
+
job_uuid=queued_job.queued_job_uuid,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return execute_job(
|
|
110
|
+
args=args,
|
|
111
|
+
profile_metadata=job_details.profile_metadata,
|
|
112
|
+
job_definition=job_details.job_definition,
|
|
113
|
+
)
|
|
114
|
+
except BaseException as e:
|
|
115
|
+
job_logger.log_exception(e)
|
|
116
|
+
return job_definition_t.JobResult(success=False)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from importlib import resources
|
|
3
|
+
|
|
4
|
+
from pkgs.argument_parser import CachedParser
|
|
5
|
+
from uncountable.core import environment
|
|
6
|
+
from uncountable.types import integration_server_t, job_definition_t
|
|
7
|
+
|
|
8
|
+
profile_parser = CachedParser(job_definition_t.ProfileDefinition)
|
|
9
|
+
|
|
10
|
+
_DEFAULT_PROFILE_ENV = integration_server_t.IntegrationEnvironment.PROD
|
|
11
|
+
_IGNORED_PROFILE_FOLDERS = ["__pycache__"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@functools.cache
|
|
15
|
+
def load_profiles() -> list[job_definition_t.ProfileMetadata]:
|
|
16
|
+
profiles_module = environment.get_profiles_module()
|
|
17
|
+
integration_envs = environment.get_integration_envs()
|
|
18
|
+
profiles = [
|
|
19
|
+
entry
|
|
20
|
+
for entry in resources.files(profiles_module).iterdir()
|
|
21
|
+
if entry.is_dir() and entry.name not in _IGNORED_PROFILE_FOLDERS
|
|
22
|
+
]
|
|
23
|
+
profile_details: list[job_definition_t.ProfileMetadata] = []
|
|
24
|
+
seen_job_ids: set[str] = set()
|
|
25
|
+
for profile_file in profiles:
|
|
26
|
+
profile_name = profile_file.name
|
|
27
|
+
try:
|
|
28
|
+
definition = profile_parser.parse_yaml_resource(
|
|
29
|
+
package=f"{profiles_module}.{profile_name}",
|
|
30
|
+
resource="profile.yaml",
|
|
31
|
+
)
|
|
32
|
+
for job in definition.jobs:
|
|
33
|
+
if job.id in seen_job_ids:
|
|
34
|
+
raise Exception(f"multiple jobs with id {job.id}")
|
|
35
|
+
seen_job_ids.add(job.id)
|
|
36
|
+
|
|
37
|
+
if definition.environments is not None:
|
|
38
|
+
for integration_env in integration_envs:
|
|
39
|
+
environment_config = definition.environments.get(integration_env)
|
|
40
|
+
if environment_config is not None:
|
|
41
|
+
profile_details.append(
|
|
42
|
+
job_definition_t.ProfileMetadata(
|
|
43
|
+
name=profile_name,
|
|
44
|
+
jobs=definition.jobs,
|
|
45
|
+
base_url=environment_config.base_url,
|
|
46
|
+
auth_retrieval=environment_config.auth_retrieval,
|
|
47
|
+
client_options=environment_config.client_options,
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
elif _DEFAULT_PROFILE_ENV in integration_envs:
|
|
51
|
+
assert (
|
|
52
|
+
definition.base_url is not None
|
|
53
|
+
and definition.auth_retrieval is not None
|
|
54
|
+
), f"define environments in profile.yaml for {profile_name}"
|
|
55
|
+
profile_details.append(
|
|
56
|
+
job_definition_t.ProfileMetadata(
|
|
57
|
+
name=profile_name,
|
|
58
|
+
jobs=definition.jobs,
|
|
59
|
+
base_url=definition.base_url,
|
|
60
|
+
auth_retrieval=definition.auth_retrieval,
|
|
61
|
+
client_options=definition.client_options,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
except FileNotFoundError as e:
|
|
65
|
+
print(f"WARN: profile.yaml not found for {profile_name}", e)
|
|
66
|
+
continue
|
|
67
|
+
return profile_details
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import multiprocessing
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import UTC
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
|
|
10
|
+
from opentelemetry.trace import get_current_span
|
|
11
|
+
|
|
12
|
+
from uncountable.core.environment import get_local_admin_server_port
|
|
13
|
+
from uncountable.integration.entrypoint import main as cron_target
|
|
14
|
+
from uncountable.integration.queue_runner.command_server import (
|
|
15
|
+
CommandServerTimeout,
|
|
16
|
+
check_health,
|
|
17
|
+
)
|
|
18
|
+
from uncountable.integration.queue_runner.queue_runner import start_queue_runner
|
|
19
|
+
from uncountable.integration.telemetry import Logger
|
|
20
|
+
|
|
21
|
+
SHUTDOWN_TIMEOUT_SECS = 30
|
|
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
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(kw_only=True)
|
|
33
|
+
class ProcessInfo:
|
|
34
|
+
name: ProcessName
|
|
35
|
+
process: AnyProcess
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_alive(self) -> bool:
|
|
39
|
+
match self.process:
|
|
40
|
+
case multiprocessing.Process():
|
|
41
|
+
return self.process.is_alive()
|
|
42
|
+
case subprocess.Popen():
|
|
43
|
+
return self.process.poll() is None
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def pid(self) -> int | None:
|
|
47
|
+
return self.process.pid
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def exitcode(self) -> int | None:
|
|
51
|
+
match self.process:
|
|
52
|
+
case multiprocessing.Process():
|
|
53
|
+
return self.process.exitcode
|
|
54
|
+
case subprocess.Popen():
|
|
55
|
+
return self.process.poll()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def handle_shutdown(logger: Logger, processes: dict[ProcessName, ProcessInfo]) -> None:
|
|
59
|
+
logger.log_info("received shutdown command, shutting down sub-processes")
|
|
60
|
+
for proc_info in processes.values():
|
|
61
|
+
if proc_info.is_alive:
|
|
62
|
+
proc_info.process.terminate()
|
|
63
|
+
|
|
64
|
+
shutdown_start = time.time()
|
|
65
|
+
still_living_processes = list(processes.values())
|
|
66
|
+
while (
|
|
67
|
+
time.time() - shutdown_start < SHUTDOWN_TIMEOUT_SECS
|
|
68
|
+
and len(still_living_processes) > 0
|
|
69
|
+
):
|
|
70
|
+
current_loop_processes = [*still_living_processes]
|
|
71
|
+
logger.log_info(
|
|
72
|
+
"waiting for sub-processes to shut down",
|
|
73
|
+
attributes={
|
|
74
|
+
"still_living_processes": [
|
|
75
|
+
proc_info.name for proc_info in still_living_processes
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
still_living_processes = []
|
|
80
|
+
for proc_info in current_loop_processes:
|
|
81
|
+
if not proc_info.is_alive:
|
|
82
|
+
logger.log_info(f"{proc_info.name} shut down successfully")
|
|
83
|
+
else:
|
|
84
|
+
still_living_processes.append(proc_info)
|
|
85
|
+
time.sleep(1)
|
|
86
|
+
|
|
87
|
+
for proc_info in still_living_processes:
|
|
88
|
+
logger.log_warning(
|
|
89
|
+
f"{proc_info.name} failed to shut down after {SHUTDOWN_TIMEOUT_SECS} seconds, forcefully terminating"
|
|
90
|
+
)
|
|
91
|
+
proc_info.process.kill()
|
|
92
|
+
|
|
93
|
+
|
|
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():
|
|
136
|
+
if not proc_info.is_alive:
|
|
137
|
+
restart_process(logger, proc_info, processes)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _wait_queue_runner_online() -> None:
|
|
141
|
+
MAX_QUEUE_RUNNER_HEALTH_CHECKS = 10
|
|
142
|
+
QUEUE_RUNNER_HEALTH_CHECK_DELAY_SECS = 1
|
|
143
|
+
|
|
144
|
+
num_attempts = 0
|
|
145
|
+
before = datetime.datetime.now(UTC)
|
|
146
|
+
while num_attempts < MAX_QUEUE_RUNNER_HEALTH_CHECKS:
|
|
147
|
+
try:
|
|
148
|
+
if check_health(port=get_local_admin_server_port()):
|
|
149
|
+
return
|
|
150
|
+
except CommandServerTimeout:
|
|
151
|
+
pass
|
|
152
|
+
num_attempts += 1
|
|
153
|
+
time.sleep(QUEUE_RUNNER_HEALTH_CHECK_DELAY_SECS)
|
|
154
|
+
after = datetime.datetime.now(UTC)
|
|
155
|
+
duration_secs = (after - before).seconds
|
|
156
|
+
raise Exception(f"queue runner failed to come online after {duration_secs} seconds")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main() -> None:
|
|
160
|
+
logger = Logger(get_current_span())
|
|
161
|
+
processes: dict[ProcessName, ProcessInfo] = {}
|
|
162
|
+
|
|
163
|
+
multiprocessing.set_start_method("forkserver")
|
|
164
|
+
|
|
165
|
+
def add_process(process: ProcessInfo) -> None:
|
|
166
|
+
processes[process.name] = process
|
|
167
|
+
logger.log_info(f"started process {process.name}")
|
|
168
|
+
|
|
169
|
+
runner_process = multiprocessing.Process(target=start_queue_runner)
|
|
170
|
+
runner_process.start()
|
|
171
|
+
add_process(ProcessInfo(name=ProcessName.QUEUE_RUNNER, process=runner_process))
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
_wait_queue_runner_online()
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.log_exception(e)
|
|
177
|
+
handle_shutdown(logger, processes=processes)
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
cron_process = multiprocessing.Process(target=cron_target)
|
|
181
|
+
cron_process.start()
|
|
182
|
+
add_process(ProcessInfo(name=ProcessName.CRON_SERVER, process=cron_process))
|
|
183
|
+
|
|
184
|
+
uwsgi_process = subprocess.Popen([
|
|
185
|
+
"uwsgi",
|
|
186
|
+
"--die-on-term",
|
|
187
|
+
])
|
|
188
|
+
add_process(ProcessInfo(name=ProcessName.UWSGI, process=uwsgi_process))
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
while True:
|
|
192
|
+
check_process_alive(logger, processes=processes)
|
|
193
|
+
time.sleep(1)
|
|
194
|
+
except KeyboardInterrupt:
|
|
195
|
+
handle_shutdown(logger, processes=processes)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
main()
|
|
@@ -5,6 +5,8 @@ import os
|
|
|
5
5
|
|
|
6
6
|
import boto3
|
|
7
7
|
|
|
8
|
+
from pkgs.argument_parser import CachedParser
|
|
9
|
+
from uncountable.types import overrides_t
|
|
8
10
|
from uncountable.types.job_definition_t import ProfileMetadata
|
|
9
11
|
from uncountable.types.secret_retrieval_t import (
|
|
10
12
|
SecretRetrieval,
|
|
@@ -13,7 +15,7 @@ from uncountable.types.secret_retrieval_t import (
|
|
|
13
15
|
)
|
|
14
16
|
|
|
15
17
|
|
|
16
|
-
class SecretRetrievalError(
|
|
18
|
+
class SecretRetrievalError(Exception):
|
|
17
19
|
def __init__(
|
|
18
20
|
self, secret_retrieval: SecretRetrieval, message: str | None = None
|
|
19
21
|
) -> None:
|
|
@@ -46,14 +48,34 @@ def _get_aws_secret(*, secret_name: str, region_name: str, sub_key: str | None)
|
|
|
46
48
|
return str(value)
|
|
47
49
|
|
|
48
50
|
|
|
51
|
+
@functools.cache
|
|
52
|
+
def _load_secret_overrides(profile_name: str) -> dict[SecretRetrieval, str]:
|
|
53
|
+
overrides_parser = CachedParser(overrides_t.Overrides)
|
|
54
|
+
profiles_module = os.environ["UNC_PROFILES_MODULE"]
|
|
55
|
+
try:
|
|
56
|
+
overrides = overrides_parser.parse_yaml_resource(
|
|
57
|
+
package=f"{profiles_module}.{profile_name}",
|
|
58
|
+
resource="local_overrides.yaml",
|
|
59
|
+
)
|
|
60
|
+
return {
|
|
61
|
+
override.secret_retrieval: override.value for override in overrides.secrets
|
|
62
|
+
}
|
|
63
|
+
except FileNotFoundError:
|
|
64
|
+
return {}
|
|
65
|
+
|
|
66
|
+
|
|
49
67
|
def retrieve_secret(
|
|
50
68
|
secret_retrieval: SecretRetrieval, profile_metadata: ProfileMetadata
|
|
51
69
|
) -> str:
|
|
70
|
+
value_from_override = _load_secret_overrides(profile_metadata.name).get(
|
|
71
|
+
secret_retrieval
|
|
72
|
+
)
|
|
73
|
+
if value_from_override is not None:
|
|
74
|
+
return value_from_override
|
|
75
|
+
|
|
52
76
|
match secret_retrieval:
|
|
53
77
|
case SecretRetrievalEnv():
|
|
54
|
-
env_name = (
|
|
55
|
-
f"UNC_{profile_metadata.name.upper()}_{secret_retrieval.env_key.upper()}"
|
|
56
|
-
)
|
|
78
|
+
env_name = f"UNC_{profile_metadata.name.upper()}_{secret_retrieval.env_key.upper()}"
|
|
57
79
|
secret = os.environ.get(env_name)
|
|
58
80
|
if secret is None:
|
|
59
81
|
raise SecretRetrievalError(
|