diff --git a/.github/workflows/pgcmp.yml b/.github/workflows/pgcmp.yml index 3a37372ccf..756b9221df 100644 --- a/.github/workflows/pgcmp.yml +++ b/.github/workflows/pgcmp.yml @@ -120,7 +120,12 @@ jobs: env: AERIE_USERNAME: "${{secrets.AERIE_USERNAME}}" AERIE_PASSWORD: "${{secrets.AERIE_PASSWORD}}" - - name: Dump Databases + - name: Clone PGCMP + uses: actions/checkout@v4 + with: + repository: cbbrowne/pgcmp + path: pgcmp + - name: Dump Migrated Database env: AERIE_USERNAME: "${{secrets.AERIE_USERNAME}}" AERIE_PASSWORD: "${{secrets.AERIE_PASSWORD}}" @@ -193,7 +198,7 @@ jobs: with: repository: cbbrowne/pgcmp path: pgcmp - - name: Dump Databases + - name: Dump Current Database env: AERIE_USERNAME: "${{secrets.AERIE_USERNAME}}" AERIE_PASSWORD: "${{secrets.AERIE_PASSWORD}}" @@ -224,7 +229,7 @@ jobs: env: AERIE_USERNAME: "${{secrets.AERIE_USERNAME}}" AERIE_PASSWORD: "${{secrets.AERIE_PASSWORD}}" - - name: Dump Databases + - name: Dump Migrated Database env: AERIE_USERNAME: "${{secrets.AERIE_USERNAME}}" AERIE_PASSWORD: "${{secrets.AERIE_PASSWORD}}" diff --git a/deployment/aerie_db_migration.py b/deployment/aerie_db_migration.py index 28d05cd206..3dbe829ba5 100755 --- a/deployment/aerie_db_migration.py +++ b/deployment/aerie_db_migration.py @@ -89,7 +89,7 @@ def step_by_step_migration(db_migration, apply): return input("Press Enter to continue...") -def bulk_migration(db_migration, apply): +def bulk_migration(db_migration, apply, current_version): # Migrate the database exit_with = 0 if apply: @@ -98,10 +98,15 @@ def bulk_migration(db_migration, apply): if exit_code != 0: exit_with = 1 else: - os.system(f'hasura migrate apply --goto 1 --database-name {db_migration.db_name} --dry-run --log-level WARN &&' - f'hasura migrate apply --down 1 --database-name {db_migration.db_name} --dry-run --log-level WARN') - exit_code = os.system(f'hasura migrate apply --goto 1 --database-name {db_migration.db_name} &&' - f'hasura migrate apply --down 1 --database-name {db_migration.db_name}') + # Performing GOTO 1 when the database is at migration 1 will cause Hasura to attempt to reapply migration 1 + if current_version == 1: + os.system(f'hasura migrate apply --down 1 --database-name {db_migration.db_name} --dry-run --log-level WARN') + exit_code = os.system(f'hasura migrate apply --down 1 --database-name {db_migration.db_name}') + else: + os.system(f'hasura migrate apply --goto 1 --database-name {db_migration.db_name} --dry-run --log-level WARN &&' + f'hasura migrate apply --down 1 --database-name {db_migration.db_name} --dry-run --log-level WARN') + exit_code = os.system(f'hasura migrate apply --goto 1 --database-name {db_migration.db_name} &&' + f'hasura migrate apply --down 1 --database-name {db_migration.db_name}') if exit_code != 0: exit_with = 1 @@ -128,7 +133,9 @@ def mark_current_version(username, password, netloc): # Mark everything up to that as applied for i in range(0, current_schema+1): - os.system('hasura migrate apply --skip-execution --version '+str(i)+' --database-name aerie >/dev/null 2>&1') + os.system('hasura migrate apply --skip-execution --version '+str(i)+' --database-name Aerie >/dev/null 2>&1') + + return current_schema def main(): # Create a cli parser @@ -227,7 +234,7 @@ def main(): os.chdir(HASURA_PATH) # Mark all migrations previously applied to the databases to be updated as such - mark_current_version(username, password, args.network_location) + current_version = mark_current_version(username, password, args.network_location) clear_screen() print(f'\n###############################' @@ -238,7 +245,7 @@ def main(): # Go step-by-step through the migrations available for the selected database step_by_step_migration(migration, args.apply) else: - bulk_migration(migration, args.apply) + bulk_migration(migration, args.apply, current_version) if __name__ == "__main__": main() diff --git a/deployment/hasura/metadata/databases/tables/hasura/refresh_activity_type_logs.yaml b/deployment/hasura/metadata/databases/tables/hasura/refresh_activity_type_logs.yaml new file mode 100644 index 0000000000..76c4dfab77 --- /dev/null +++ b/deployment/hasura/metadata/databases/tables/hasura/refresh_activity_type_logs.yaml @@ -0,0 +1,30 @@ +table: + name: refresh_activity_type_logs + schema: hasura +configuration: + custom_name: "refresh_activity_type_logs" +object_relationships: +- name: model + using: + manual_configuration: + remote_table: + name: mission_model + schema: merlin + column_mapping: + model_id: id +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {} + allow_aggregations: true diff --git a/deployment/hasura/metadata/databases/tables/hasura/refresh_model_parameter_logs.yaml b/deployment/hasura/metadata/databases/tables/hasura/refresh_model_parameter_logs.yaml new file mode 100644 index 0000000000..71d4b3def9 --- /dev/null +++ b/deployment/hasura/metadata/databases/tables/hasura/refresh_model_parameter_logs.yaml @@ -0,0 +1,30 @@ +table: + name: refresh_model_parameter_logs + schema: hasura +configuration: + custom_name: "refresh_model_parameter_logs" +object_relationships: +- name: model + using: + manual_configuration: + remote_table: + name: mission_model + schema: merlin + column_mapping: + model_id: id +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {} + allow_aggregations: true diff --git a/deployment/hasura/metadata/databases/tables/hasura/refresh_resource_types_logs.yaml b/deployment/hasura/metadata/databases/tables/hasura/refresh_resource_types_logs.yaml new file mode 100644 index 0000000000..a442e8b65e --- /dev/null +++ b/deployment/hasura/metadata/databases/tables/hasura/refresh_resource_types_logs.yaml @@ -0,0 +1,30 @@ +table: + name: refresh_resource_type_logs + schema: hasura +configuration: + custom_name: "refresh_resource_type_logs" +object_relationships: +- name: model + using: + manual_configuration: + remote_table: + name: mission_model + schema: merlin + column_mapping: + model_id: id +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {} + allow_aggregations: true diff --git a/deployment/hasura/metadata/databases/tables/merlin/mission_model.yaml b/deployment/hasura/metadata/databases/tables/merlin/mission_model.yaml index cdbb5b3115..64969023ea 100644 --- a/deployment/hasura/metadata/databases/tables/merlin/mission_model.yaml +++ b/deployment/hasura/metadata/databases/tables/merlin/mission_model.yaml @@ -57,6 +57,30 @@ array_relationships: table: name: scheduling_model_specification_goals schema: scheduler +- name: refresh_activity_type_logs + using: + manual_configuration: + remote_table: + name: refresh_activity_type_logs + schema: hasura + column_mapping: + id: model_id +- name: refresh_model_parameter_logs + using: + manual_configuration: + remote_table: + name: refresh_model_parameter_logs + schema: hasura + column_mapping: + id: model_id +- name: refresh_resource_type_logs + using: + manual_configuration: + remote_table: + name: refresh_resource_type_logs + schema: hasura + column_mapping: + id: model_id select_permissions: - role: aerie_admin permission: diff --git a/deployment/hasura/metadata/databases/tables/tables.yaml b/deployment/hasura/metadata/databases/tables/tables.yaml index cca48af2fb..8751d21852 100644 --- a/deployment/hasura/metadata/databases/tables/tables.yaml +++ b/deployment/hasura/metadata/databases/tables/tables.yaml @@ -46,6 +46,9 @@ - "!include hasura/get_conflicting_activities_return_value.yaml" - "!include hasura/get_non_conflicting_activities_return_value.yaml" - "!include hasura/get_plan_history_return_value.yaml" +- "!include hasura/refresh_activity_type_logs.yaml" +- "!include hasura/refresh_model_parameter_logs.yaml" +- "!include hasura/refresh_resource_types_logs.yaml" - "!include hasura/resource_at_start_offset_return_value.yaml" - "!include hasura/withdraw_merge_request_return_value.yaml" diff --git a/deployment/hasura/migrations/Aerie/1_hasura_events_views/down.sql b/deployment/hasura/migrations/Aerie/1_hasura_events_views/down.sql new file mode 100644 index 0000000000..f95854e1df --- /dev/null +++ b/deployment/hasura/migrations/Aerie/1_hasura_events_views/down.sql @@ -0,0 +1,7 @@ +drop view hasura.refresh_resource_type_logs; +drop view hasura.refresh_model_parameter_logs; +drop view hasura.refresh_activity_type_logs; + +drop function hasura.get_event_logs(_trigger_name text); + +call migrations.mark_migration_rolled_back('1'); diff --git a/deployment/hasura/migrations/Aerie/1_hasura_events_views/up.sql b/deployment/hasura/migrations/Aerie/1_hasura_events_views/up.sql new file mode 100644 index 0000000000..d924db5ea8 --- /dev/null +++ b/deployment/hasura/migrations/Aerie/1_hasura_events_views/up.sql @@ -0,0 +1,59 @@ +create function hasura.get_event_logs(_trigger_name text) +returns table ( + model_id int, + model_name text, + model_version text, + triggering_user text, + delivered boolean, + success boolean, + tries int, + created_at timestamp, + next_retry_at timestamp, + status int, + error jsonb, + error_message text, + error_type text +) +stable +security invoker +language plpgsql as $$ +begin + return query ( + select + (el.payload->'data'->'new'->>'id')::int as model_id, + el.payload->'data'->'new'->>'name' as model_name, + el.payload->'data'->'new'->>'version' as model_version, + el.payload->'session_variables'->>'x-hasura-user-id' as triggering_user, + el.delivered, + eil.status is not distinct from 200 as success, -- is not distinct from to catch `null` + el.tries, + el.created_at, + el.next_retry_at, + eil.status, + (eil.response -> 'data'->> 'message')::jsonb as error, + (eil.response -> 'data'->> 'message')::jsonb->>'message' as error_message, + (eil.response -> 'data'->> 'message')::jsonb->>'type' as error_type + from hdb_catalog.event_log el + join hdb_catalog.event_invocation_logs eil on el.id = eil.event_id + where trigger_name = _trigger_name); +end; +$$; +comment on function hasura.get_event_logs(_trigger_name text) is e'' + 'Get the logs for every run of a Hasura event with the specified trigger name.'; + +create view hasura.refresh_activity_type_logs as + select * from hasura.get_event_logs('refreshActivityTypes'); +comment on view hasura.refresh_activity_type_logs is e'' + 'View containing logs for every run of the Hasura event `refreshActivityTypes`.'; + +create view hasura.refresh_model_parameter_logs as + select * from hasura.get_event_logs('refreshModelParameters'); +comment on view hasura.refresh_model_parameter_logs is e'' + 'View containing logs for every run of the Hasura event `refreshModelParameters`.'; + +create view hasura.refresh_resource_type_logs as + select * from hasura.get_event_logs('refreshResourceTypes'); +comment on view hasura.refresh_resource_type_logs is e'' + 'View containing logs for every run of the Hasura event `refreshResourceTypes`.'; + +call migrations.mark_migration_applied('1'); diff --git a/deployment/postgres-init-db/sql/applied_migrations.sql b/deployment/postgres-init-db/sql/applied_migrations.sql index 81b3adbfda..6191790699 100644 --- a/deployment/postgres-init-db/sql/applied_migrations.sql +++ b/deployment/postgres-init-db/sql/applied_migrations.sql @@ -3,3 +3,4 @@ This file denotes which migrations occur "before" this version of the schema. */ call migrations.mark_migration_applied('0'); +call migrations.mark_migration_applied('1'); diff --git a/deployment/postgres-init-db/sql/init_hasura.sql b/deployment/postgres-init-db/sql/init_hasura.sql index b9d4150570..48c8994c3a 100644 --- a/deployment/postgres-init-db/sql/init_hasura.sql +++ b/deployment/postgres-init-db/sql/init_hasura.sql @@ -13,4 +13,7 @@ begin; \ir functions/hasura/plan_branching_functions.sql \ir functions/hasura/plan_merge_functions.sql \ir functions/hasura/snapshot_functions.sql + + -- Event Views + \ir views/hasura/hasura_event_logs.sql end; diff --git a/deployment/postgres-init-db/sql/views/hasura/hasura_event_logs.sql b/deployment/postgres-init-db/sql/views/hasura/hasura_event_logs.sql new file mode 100644 index 0000000000..60c48cc57e --- /dev/null +++ b/deployment/postgres-init-db/sql/views/hasura/hasura_event_logs.sql @@ -0,0 +1,60 @@ +create function hasura.get_event_logs(_trigger_name text) +returns table ( + model_id int, + model_name text, + model_version text, + triggering_user text, + delivered boolean, + success boolean, + tries int, + created_at timestamp, + next_retry_at timestamp, + status int, + error jsonb, + error_message text, + error_type text +) +stable +security invoker +language plpgsql as $$ +begin + return query ( + select + (el.payload->'data'->'new'->>'id')::int as model_id, + el.payload->'data'->'new'->>'name' as model_name, + el.payload->'data'->'new'->>'version' as model_version, + el.payload->'session_variables'->>'x-hasura-user-id' as triggering_user, + el.delivered, + eil.status is not distinct from 200 as success, -- is not distinct from to catch `null` + el.tries, + el.created_at, + el.next_retry_at, + eil.status, + (eil.response -> 'data'->> 'message')::jsonb as error, + (eil.response -> 'data'->> 'message')::jsonb->>'message' as error_message, + (eil.response -> 'data'->> 'message')::jsonb->>'type' as error_type + from hdb_catalog.event_log el + join hdb_catalog.event_invocation_logs eil on el.id = eil.event_id + where trigger_name = _trigger_name); +end; +$$; +comment on function hasura.get_event_logs(_trigger_name text) is e'' + 'Get the logs for every run of a Hasura event with the specified trigger name.'; + +create view hasura.refresh_activity_type_logs as + select * from hasura.get_event_logs('refreshActivityTypes'); +comment on view hasura.refresh_activity_type_logs is e'' + 'View containing logs for every run of the Hasura event `refreshActivityTypes`.'; + +create view hasura.refresh_model_parameter_logs as + select * from hasura.get_event_logs('refreshModelParameters'); +comment on view hasura.refresh_model_parameter_logs is e'' + 'View containing logs for every run of the Hasura event `refreshModelParameters`.'; + +create view hasura.refresh_resource_type_logs as + select * from hasura.get_event_logs('refreshResourceTypes'); +comment on view hasura.refresh_resource_type_logs is e'' + 'View containing logs for every run of the Hasura event `refreshResourceTypes`.'; + + + diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/MissionModelTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/MissionModelTests.java index 7947734f12..7b0776e285 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/MissionModelTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/MissionModelTests.java @@ -48,7 +48,6 @@ void beforeAll() throws IOException, InterruptedException { "aerie_e2e_tests", "Mission Model Tests"); } - Thread.sleep(1000); } @AfterAll @@ -363,4 +362,64 @@ void activityTypesAreUploaded() throws IOException{ assertEquals(expectedComputedAttributes, actualComputedAttributes); } } + + /** + * The logs for the Hasura events triggered during model upload are accessible. + */ + @Test + void hasuraEventLogsAreAccessible() throws IOException { + final var modelLogs = hasura.awaitModelEventLogs(modelId); + + assertEquals(modelId, modelLogs.modelId()); + assertEquals("Banananation (e2e tests)", modelLogs.modelName()); + assertEquals("Mission Model Tests", modelLogs.modelVersion()); + + // Check Activity Type Refresh Event Logs + final var activityTypeRefreshLogs = modelLogs.refreshActivityTypesLogs(); + assertEquals(1, activityTypeRefreshLogs.size()); + final var activityTypeLog = activityTypeRefreshLogs.get(0); + + assertEquals("Aerie Legacy", activityTypeLog.triggeringUser()); + + assertTrue(activityTypeLog.delivered()); + assertTrue(activityTypeLog.success()); + assertEquals(1, activityTypeLog.tries()); + assertEquals(200, activityTypeLog.status()); + + assertTrue(activityTypeLog.error().isEmpty()); + assertTrue(activityTypeLog.errorMessage().isEmpty()); + assertTrue(activityTypeLog.errorType().isEmpty()); + + // Check Model Parameter Refresh Event Logs + final var modelParamRefreshLogs = modelLogs.refreshModelParamsLogs(); + assertEquals(1, modelParamRefreshLogs.size()); + final var modelParamLog = modelParamRefreshLogs.get(0); + + assertEquals("Aerie Legacy", modelParamLog.triggeringUser()); + + assertTrue(modelParamLog.delivered()); + assertTrue(modelParamLog.success()); + assertEquals(1, modelParamLog.tries()); + assertEquals(200, modelParamLog.status()); + + assertTrue(modelParamLog.error().isEmpty()); + assertTrue(modelParamLog.errorMessage().isEmpty()); + assertTrue(modelParamLog.errorType().isEmpty()); + + // Check Resource Type Refresh Event Logs + final var resourceTypeRefreshLogs = modelLogs.refreshResourceTypesLogs(); + assertEquals(1, resourceTypeRefreshLogs.size()); + final var resourceTypeLog = resourceTypeRefreshLogs.get(0); + + assertEquals("Aerie Legacy", resourceTypeLog.triggeringUser()); + + assertTrue(resourceTypeLog.delivered()); + assertTrue(resourceTypeLog.success()); + assertEquals(1, resourceTypeLog.tries()); + assertEquals(200, resourceTypeLog.status()); + + assertTrue(resourceTypeLog.error().isEmpty()); + assertTrue(resourceTypeLog.errorMessage().isEmpty()); + assertTrue(resourceTypeLog.errorType().isEmpty()); + } } diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java index fceb29b69a..d55ca76f93 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java @@ -97,7 +97,7 @@ void afterAll() { } @BeforeEach - void beforeEach() throws IOException { + void beforeEach() throws IOException, InterruptedException { // Insert the Mission Model try (final var gateway = new GatewayRequests(playwright)) { modelId = hasura.createMissionModel( @@ -105,8 +105,6 @@ void beforeEach() throws IOException { "Banananation (e2e tests)", "aerie_e2e_tests", "Scheduling Tests"); - } catch (InterruptedException e) { - throw new RuntimeException(e); } // Insert the Plan planId = hasura.createPlan( diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ModelEventLogs.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ModelEventLogs.java new file mode 100644 index 0000000000..b342e0b153 --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ModelEventLogs.java @@ -0,0 +1,58 @@ +package gov.nasa.jpl.aerie.e2e.types; + +import javax.json.JsonObject; +import java.util.List; +import java.util.Optional; + +public record ModelEventLogs( + int modelId, + String modelName, + String modelVersion, + List refreshActivityTypesLogs, + List refreshModelParamsLogs, + List refreshResourceTypesLogs +) { + public record EventLog( + String triggeringUser, + boolean delivered, + boolean success, + int tries, + String createdAt, + int status, + Optional error, + Optional errorMessage, + Optional errorType + ) + { + public static EventLog fromJSON(JsonObject json) { + final Optional error = json.isNull("error") ? + Optional.empty() : Optional.of(json.getJsonObject("error")); + final Optional errorMsg = json.isNull("error_message") ? + Optional.empty() : Optional.of(json.getString("error_message")); + final Optional errorType = json.isNull("error") ? + Optional.empty() : Optional.of(json.getString("error")); + + return new EventLog( + json.getString("triggering_user"), + json.getBoolean("delivered"), + json.getBoolean("success"), + json.getInt("tries"), + json.getString("created_at"), + json.getInt("status"), + error, + errorMsg, + errorType); + } + } + + public static ModelEventLogs fromJSON(JsonObject json) { + return new ModelEventLogs( + json.getInt("id"), + json.getString("name"), + json.getString("version"), + json.getJsonArray("refresh_activity_type_logs").getValuesAs(EventLog::fromJSON), + json.getJsonArray("refresh_model_parameter_logs").getValuesAs(EventLog::fromJSON), + json.getJsonArray("refresh_resource_type_logs").getValuesAs(EventLog::fromJSON) + ); + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java index e8ce00eafb..08079cc25b 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java @@ -265,6 +265,47 @@ query getExternalDataset($plan_id: Int!, $dataset_id: Int!) { } } }"""), + GET_MODEL_EVENT_LOGS(""" + query getModelLogs($modelId: Int!) { + mission_model: mission_model_by_pk(id:$modelId) { + id + name + version + refresh_activity_type_logs(order_by: {created_at: desc}) { + triggering_user + delivered + success + tries + created_at + status + error + error_message + error_type + } + refresh_model_parameter_logs(order_by: {created_at: desc}) { + triggering_user + delivered + success + tries + created_at + status + error + error_message + error_type + } + refresh_resource_type_logs(order_by: {created_at: desc}) { + triggering_user + delivered + success + tries + created_at + status + error + error_message + error_type + } + } + }"""), GET_PLAN(""" query GetPlan($id: Int!) { plan: plan_by_pk(id: $id) { diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java index 2e8c0e5f6e..d9f2666b64 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java @@ -109,10 +109,12 @@ public int createMissionModel(int jarId, String name, String mission, String ver .add("version", version); final var variables = Json.createObjectBuilder().add("model", insertModelBuilder).build(); final var data = makeRequest(GQL.CREATE_MISSION_MODEL, variables).getJsonObject("insert_mission_model_one"); - // Delay 1.25s to guarantee all events associated with model upload have finished + final int modelId = data.getInt("id"); + + // Wait for all events associated with model upload to finish // Necessary for TS compilation - Thread.sleep(1250); - return data.getInt("id"); + awaitModelEventLogs(modelId); + return modelId; } public void deleteMissionModel(int id) throws IOException { @@ -143,6 +145,39 @@ public List getActivityTypes(int missionModelId) throws IOExceptio final var data = makeRequest(GQL.GET_ACTIVITY_TYPES, variables); return data.getJsonArray("activity_type").getValuesAs(ActivityType::fromJSON); } + + /** + * Get the Hasura Event Logs for the mission model with a timeout of 30 seconds. + * @param modelId the mission model to get logs for + */ + public ModelEventLogs awaitModelEventLogs(int modelId) throws IOException { + return awaitModelEventLogs(modelId, 30); + } + + /** + * Get the Hasura Event Logs for the mission model. + * @param modelId the mission model to get logs for + * @param timeout the amount of time to wait for at least one log of each type + */ + public ModelEventLogs awaitModelEventLogs(int modelId, int timeout) throws IOException { + final var variables = Json.createObjectBuilder().add("modelId", modelId).build(); + + for(int i = 0; i < timeout; ++i){ + final var logs = makeRequest(GQL.GET_MODEL_EVENT_LOGS, variables).getJsonObject("mission_model"); + if(logs.getJsonArray("refresh_activity_type_logs").isEmpty() + || logs.getJsonArray("refresh_model_parameter_logs").isEmpty() + || logs.getJsonArray("refresh_resource_type_logs").isEmpty()) { + try { + Thread.sleep(1000); // 1s + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } else { + return ModelEventLogs.fromJSON(logs); + } + } + throw new TimeoutError("One or more mission model Hausra events did not return after " + timeout + " seconds"); + } //endregion //region Plan @@ -317,7 +352,7 @@ public SimulationResponse awaitSimulation(int planId, int timeout) throws IOExce throw new TimeoutError("Simulation timed out after " + timeout + " seconds"); } - /** + /** * Simulate the specified plan, potentially forcibly, with a timeout of 30 seconds * @param planId the plan to simulate * @param force whether to forcibly resimulate in the event of an existing dataset.