diff --git a/.github/workflows/backend_tests.yml b/.github/workflows/backend_tests.yml index fff033ed3..97cf7ca44 100644 --- a/.github/workflows/backend_tests.yml +++ b/.github/workflows/backend_tests.yml @@ -156,7 +156,7 @@ jobs: - name: Upload coverage data # pin to upload coverage from only one matrix entry, otherwise coverage gets confused later if: always() && matrix.session == 'tests' && matrix.python == '3.11' && matrix.os == 'ubuntu-latest' && matrix.database == 'mysql' - uses: "actions/upload-artifact@v3.0.0" + uses: "actions/upload-artifact@v3" # this action doesn't seem to respect working-directory so include working-directory value in path with: name: coverage-data @@ -164,14 +164,14 @@ jobs: - name: Upload documentation if: matrix.session == 'docs-build' - uses: actions/upload-artifact@v3.0.0 + uses: actions/upload-artifact@v3 with: name: docs path: docs/_build - name: Upload logs if: failure() && matrix.session == 'tests' - uses: "actions/upload-artifact@v3.0.0" + uses: "actions/upload-artifact@v3" with: name: logs-${{matrix.python}}-${{matrix.os}}-${{matrix.database}} path: "./log/*.log" diff --git a/.github/workflows/frontend_tests.yml b/.github/workflows/frontend_tests.yml index 405b359c2..4e80311b1 100644 --- a/.github/workflows/frontend_tests.yml +++ b/.github/workflows/frontend_tests.yml @@ -108,21 +108,21 @@ jobs: run: ./bin/get_logs_from_docker_compose >./log/docker_compose.log - name: Upload logs if: failure() - uses: "actions/upload-artifact@v3.0.0" + uses: "actions/upload-artifact@v3" with: name: spiffworkflow-backend-logs path: "./spiffworkflow-backend/log/*.log" # https://github.com/cypress-io/github-action#artifacts - name: upload_screenshots - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: failure() with: name: cypress-screenshots path: ./spiffworkflow-frontend/cypress/screenshots # Test run video was always captured, so this action uses "always()" condition - name: upload_videos - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: failure() with: name: cypress-videos diff --git a/spiffworkflow-backend/keycloak/realm_exports/spiffworkflow-realm.json b/spiffworkflow-backend/keycloak/realm_exports/spiffworkflow-realm.json index 1c57944b8..99e651b9a 100644 --- a/spiffworkflow-backend/keycloak/realm_exports/spiffworkflow-realm.json +++ b/spiffworkflow-backend/keycloak/realm_exports/spiffworkflow-realm.json @@ -396,7 +396,7 @@ "otpPolicyLookAheadWindow" : 1, "otpPolicyPeriod" : 30, "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppGoogleName", "totpAppFreeOTPName" ], + "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName" ], "webAuthnPolicyRpEntityName" : "keycloak", "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], "webAuthnPolicyRpId" : "", @@ -991,6 +991,29 @@ "realmRoles" : [ "default-roles-spiffworkflow" ], "notBefore" : 0, "groups" : [ ] + }, { + "id" : "7b86b997-de98-478c-8550-cfca65e40c33", + "createdTimestamp" : 1679060366901, + "username" : "core18.contributor", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "email" : "core18.contributor@status.im", + "attributes" : { + "spiffworkflow-employeeid" : [ "233" ] + }, + "credentials" : [ { + "id" : "55ca2bd7-6f60-4f04-be21-df6300ca9442", + "type" : "password", + "createdDate" : 1679060366954, + "secretData" : "{\"value\":\"hC/O8LJ8/y/nXLmRFgRazOX9PXMHkowYH1iHUB4Iw9jzc8IMMv8dFrxu7XBklfyz7CPc1bmgl0k29jygRZYHlg==\",\"salt\":\"4R17tmLrHWyFAMvrfLMETQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] }, { "id" : "3b81b45e-759b-4d7a-aa90-adf7b447208c", "createdTimestamp" : 1676302140358, @@ -1505,6 +1528,29 @@ "realmRoles" : [ "default-roles-spiffworkflow" ], "notBefore" : 0, "groups" : [ ] + }, { + "id" : "d123d384-66a4-4db5-9dbb-d73c12047001", + "createdTimestamp" : 1678997616280, + "username" : "finance.project-lead", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "email" : "finance.project-lead@status.im", + "attributes" : { + "spiffworkflow-employeeid" : [ "128" ] + }, + "credentials" : [ { + "id" : "b680f5c5-c2de-4255-9d23-7e18cff3ac4e", + "type" : "password", + "createdDate" : 1678997616336, + "secretData" : "{\"value\":\"4kasmb11Sv62rInh8eFUhS3rGYNymzsvxzfsEIWGYhnlisYuo1iTS2opv/kET/NyJlsYrfwc7yrIqSHvkUHkkA==\",\"salt\":\"q/ees3a4K+3K11olnfPzCQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-spiffworkflow" ], + "notBefore" : 0, + "groups" : [ ] }, { "id" : "f6d2488a-446c-493b-bbe8-210ede6f3e42", "createdTimestamp" : 1674148694899, @@ -4578,7 +4624,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-address-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "oidc-full-name-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "saml-user-property-mapper" ] } }, { "id" : "d68e938d-dde6-47d9-bdc8-8e8523eb08cd", @@ -4596,7 +4642,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "saml-user-property-mapper" ] + "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper" ] } }, { "id" : "3854361d-3fe5-47fb-9417-a99592e3dc5c", @@ -4686,7 +4732,7 @@ "internationalizationEnabled" : false, "supportedLocales" : [ ], "authenticationFlows" : [ { - "id" : "04b09640-f53c-4c1b-b2b1-8cac25afc2bb", + "id" : "38a6b336-b026-46be-a8be-e8ff7b9da407", "alias" : "Account verification options", "description" : "Method with which to verity the existing account", "providerId" : "basic-flow", @@ -4708,7 +4754,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "e7c246f4-71c3-4a48-9037-72438bdcfcbb", + "id" : "eb9fe753-cd35-4e65-bb34-e83ba7059566", "alias" : "Authentication Options", "description" : "Authentication options.", "providerId" : "basic-flow", @@ -4737,7 +4783,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "6e9d415e-98f7-4459-b10b-45b08302c681", + "id" : "aa9c74f7-0426-4440-907f-4aa0f999eb1e", "alias" : "Browser - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -4759,7 +4805,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "c86b0fad-f7dd-4c58-974e-25eb83c1dacf", + "id" : "eb2a0849-c316-46bc-8b06-fd0cc50e3f32", "alias" : "Direct Grant - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -4781,7 +4827,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "cb7f4c87-a8fa-445a-a8d4-53869cdfed12", + "id" : "8f064003-823b-4be1-aa66-7324bf38c741", "alias" : "First broker login - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -4803,7 +4849,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "8fa87954-bc65-4f1e-bc55-f5bb49f59fbb", + "id" : "eef22678-b09c-4ca8-bdcf-90ea44ff0120", "alias" : "Handle Existing Account", "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId" : "basic-flow", @@ -4825,7 +4871,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "e617d826-c654-4c35-96ad-8381bd1e2298", + "id" : "4367f263-ef2c-426e-b5cd-49fff868ea1a", "alias" : "Reset - Conditional OTP", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId" : "basic-flow", @@ -4847,7 +4893,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "2e4a46ae-2813-4b71-9386-c08b2f063fa6", + "id" : "b2e9c608-1779-4c03-b32a-03c77450abae", "alias" : "User creation or linking", "description" : "Flow for the existing/non-existing user alternatives", "providerId" : "basic-flow", @@ -4870,7 +4916,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "8fa69de0-13cf-4252-899b-c59a30ebd132", + "id" : "a8c79324-1881-4bb0-a8a2-83dfd54cacd1", "alias" : "Verify Existing Account by Re-authentication", "description" : "Reauthentication of existing account", "providerId" : "basic-flow", @@ -4892,7 +4938,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "204d20f6-d9a7-49ff-a7a3-45386fb884f4", + "id" : "d1aa83c6-da36-4cb6-b6ed-f6ec556df614", "alias" : "browser", "description" : "browser based authentication", "providerId" : "basic-flow", @@ -4928,7 +4974,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "3c0c2987-65db-4920-ae44-34aba220c3fb", + "id" : "2afecfef-4bfb-4842-b338-7ed032a618d2", "alias" : "clients", "description" : "Base authentication for clients", "providerId" : "client-flow", @@ -4964,7 +5010,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "68a92113-be75-4e63-a322-8076d6c67650", + "id" : "34dc1854-4969-4065-90e6-fef38b0dea98", "alias" : "direct grant", "description" : "OpenID Connect Resource Owner Grant", "providerId" : "basic-flow", @@ -4993,7 +5039,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a630d78f-4fe1-4350-a19d-d091d1af514d", + "id" : "40557323-dbbc-48ee-9ed1-748b11c9628d", "alias" : "docker auth", "description" : "Used by Docker clients to authenticate against the IDP", "providerId" : "basic-flow", @@ -5008,7 +5054,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "f73b4437-8e82-4788-be69-e437b09b500c", + "id" : "d18b5c50-39fa-4b11-a7d2-0e6768e275c1", "alias" : "first broker login", "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId" : "basic-flow", @@ -5031,7 +5077,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "b7c8cc6d-bc1f-446e-b263-72214b2f5c56", + "id" : "976be80d-a88b-412c-8ad2-9ebe427793d4", "alias" : "forms", "description" : "Username, password, otp and other auth forms.", "providerId" : "basic-flow", @@ -5053,7 +5099,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a3bdf79f-8c7d-4bff-807d-76fa61093446", + "id" : "83b3a411-ff7c-4cba-845a-9554c536d6b1", "alias" : "http challenge", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId" : "basic-flow", @@ -5075,7 +5121,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ada41b4e-5a12-496d-aa1e-d31cf8c08226", + "id" : "1cb835a6-b38c-4f29-a6d8-d04d0a84d05e", "alias" : "registration", "description" : "registration flow", "providerId" : "basic-flow", @@ -5091,7 +5137,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "1c858bcd-2031-4056-bbf0-1fbaecdd7068", + "id" : "7ec06c82-6802-4ff4-a3ab-9b6a0b8dbc4b", "alias" : "registration form", "description" : "registration form", "providerId" : "form-flow", @@ -5127,7 +5173,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ff91e251-d85e-450b-bff7-d45be26777d5", + "id" : "f3bc2f7b-2074-4d93-9578-3abf648a6681", "alias" : "reset credentials", "description" : "Reset credentials for a user if they forgot their password or something", "providerId" : "basic-flow", @@ -5163,7 +5209,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "7b0680a2-99b9-454c-b145-f286e9d60c58", + "id" : "e62e031b-9922-4682-b867-bc5c3a4a7e99", "alias" : "saml ecp", "description" : "SAML ECP Profile Authentication Flow", "providerId" : "basic-flow", @@ -5179,13 +5225,13 @@ } ] } ], "authenticatorConfig" : [ { - "id" : "aa1e4f55-3e7f-445a-a432-7a972776d719", + "id" : "c449f0aa-5f3c-4107-9f04-3222fa93a486", "alias" : "create unique user config", "config" : { "require.password.update.after.registration" : "false" } }, { - "id" : "fd69765e-309b-4c5d-bdd5-51343427cd27", + "id" : "f7a6ed54-0ab8-4f29-9877-960bd65bf394", "alias" : "review profile config", "config" : { "update.profile.on.first.login" : "missing" diff --git a/spiffworkflow-backend/keycloak/test_user_lists/status b/spiffworkflow-backend/keycloak/test_user_lists/status index 3b57deff1..5af7736d5 100644 --- a/spiffworkflow-backend/keycloak/test_user_lists/status +++ b/spiffworkflow-backend/keycloak/test_user_lists/status @@ -21,6 +21,7 @@ core14.contributor@status.im,229 core15.contributor@status.im,230 core16.contributor@status.im,231 core17.contributor@status.im,232 +core18.contributor@status.im,233 core2.contributor@status.im,156 core3.contributor@status.im,157 core4.contributor@status.im,158 @@ -42,6 +43,7 @@ desktop3.sme@status.im,196 desktop4.sme@status.im,197 desktop5.sme@status.im,198 fin@status.im,118 +finance.project-lead@status.im,128 finance_user1@status.im fluffy.project-lead@status.im,162 harmeet@status.im,109 diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py index ca808564d..2af3e7df0 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py @@ -129,5 +129,13 @@ SPIFFWORKFLOW_BACKEND_ENGINE_STEP_DEFAULT_STRATEGY_WEB = environ.get( "SPIFFWORKFLOW_BACKEND_ENGINE_STEP_DEFAULT_STRATEGY_WEB", default="greedy" ) +SPIFFWORKFLOW_BACKEND_USER_INPUT_REQUIRED_LOCK_RETRY_TIMES = int( + environ.get("SPIFFWORKFLOW_BACKEND_USER_INPUT_REQUIRED_LOCK_RETRY_TIMES", default="3") +) + +SPIFFWORKFLOW_BACKEND_USER_INPUT_REQUIRED_LOCK_RETRY_INTERVAL_IN_SECONDS = int( + environ.get("SPIFFWORKFLOW_BACKEND_USER_INPUT_REQUIRED_LOCK_RETRY_INTERVAL_IN_SECONDS", default="1") +) + # this is only used in CI. use SPIFFWORKFLOW_BACKEND_DATABASE_URI instead for real configuration SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD = environ.get("SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD", default=None) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py index 9f883c0ae..cd84f3d40 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -173,7 +173,17 @@ def process_instance_terminate( """Process_instance_run.""" process_instance = _find_process_instance_by_id_or_raise(process_instance_id) processor = ProcessInstanceProcessor(process_instance) - processor.terminate() + + try: + processor.lock_process_instance("Web") + processor.terminate() + except (ProcessInstanceIsNotEnqueuedError, ProcessInstanceIsAlreadyLockedError) as e: + ErrorHandlingService().handle_error(processor, e) + raise e + finally: + if ProcessInstanceLockService.has_lock(process_instance.id): + processor.unlock_process_instance("Web") + return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") @@ -183,7 +193,18 @@ def process_instance_suspend( ) -> flask.wrappers.Response: """Process_instance_suspend.""" process_instance = _find_process_instance_by_id_or_raise(process_instance_id) - ProcessInstanceProcessor.suspend(process_instance) + processor = ProcessInstanceProcessor(process_instance) + + try: + processor.lock_process_instance("Web") + processor.suspend() + except (ProcessInstanceIsNotEnqueuedError, ProcessInstanceIsAlreadyLockedError) as e: + ErrorHandlingService().handle_error(processor, e) + raise e + finally: + if ProcessInstanceLockService.has_lock(process_instance.id): + processor.unlock_process_instance("Web") + return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") @@ -193,7 +214,18 @@ def process_instance_resume( ) -> flask.wrappers.Response: """Process_instance_resume.""" process_instance = _find_process_instance_by_id_or_raise(process_instance_id) - ProcessInstanceProcessor.resume(process_instance) + processor = ProcessInstanceProcessor(process_instance) + + try: + processor.lock_process_instance("Web") + processor.resume() + except (ProcessInstanceIsNotEnqueuedError, ProcessInstanceIsAlreadyLockedError) as e: + ErrorHandlingService().handle_error(processor, e) + raise e + finally: + if ProcessInstanceLockService.has_lock(process_instance.id): + processor.unlock_process_instance("Web") + return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index 7ca7c6ebe..ad9868e63 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -378,8 +378,13 @@ def task_submit_shared( only_tasks_that_can_be_completed=True, ) + retry_times = current_app.config["SPIFFWORKFLOW_BACKEND_USER_INPUT_REQUIRED_LOCK_RETRY_TIMES"] + retry_interval_in_seconds = current_app.config[ + "SPIFFWORKFLOW_BACKEND_USER_INPUT_REQUIRED_LOCK_RETRY_INTERVAL_IN_SECONDS" + ] + with sentry_sdk.start_span(op="task", description="complete_form_task"): - processor.lock_process_instance("Web") + processor.lock_process_instance("Web", retry_times, retry_interval_in_seconds) ProcessInstanceService.complete_form_task( processor=processor, spiff_task=spiff_task, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index 768ebc220..7435f39ed 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -91,9 +91,8 @@ from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.process_instance_lock_service import ( ProcessInstanceLockService, ) -from spiffworkflow_backend.services.process_instance_queue_service import ( - ProcessInstanceQueueService, -) +from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceIsAlreadyLockedError +from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceQueueService from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.service_task_service import ServiceTaskDelegate from spiffworkflow_backend.services.spec_file_service import SpecFileService @@ -1276,7 +1275,7 @@ class ProcessInstanceProcessor: self.add_step() self.save() # Saving the workflow seems to reset the status - self.suspend(self.process_instance_model) + self.suspend() def reset_process(self, spiff_step: int) -> None: """Reset a process to an earlier state.""" @@ -1319,7 +1318,7 @@ class ProcessInstanceProcessor: db.session.delete(row) self.save() - self.suspend(self.process_instance_model) + self.suspend() @staticmethod def get_parser() -> MyCustomParser: @@ -1485,8 +1484,23 @@ class ProcessInstanceProcessor: return the_status # TODO: replace with implicit/more granular locking in workflow execution service - def lock_process_instance(self, lock_prefix: str) -> None: - ProcessInstanceQueueService.dequeue(self.process_instance_model) + # TODO: remove the retry logic once all user_input_required's don't need to be locked to check timers + def lock_process_instance( + self, lock_prefix: str, retry_count: int = 0, retry_interval_in_seconds: int = 0 + ) -> None: + try: + ProcessInstanceQueueService.dequeue(self.process_instance_model) + except ProcessInstanceIsAlreadyLockedError as e: + if retry_count > 0: + current_app.logger.info( + f"process_instance_id {self.process_instance_model.id} is locked. " + f"will retry {retry_count} times with delay of {retry_interval_in_seconds}." + ) + if retry_interval_in_seconds > 0: + time.sleep(retry_interval_in_seconds) + self.lock_process_instance(lock_prefix, retry_count - 1, retry_interval_in_seconds) + else: + raise e # TODO: replace with implicit/more granular locking in workflow execution service def unlock_process_instance(self, lock_prefix: str) -> None: @@ -1923,16 +1937,14 @@ class ProcessInstanceProcessor: db.session.add(self.process_instance_model) db.session.commit() - @classmethod - def suspend(cls, process_instance: ProcessInstanceModel) -> None: + def suspend(self) -> None: """Suspend.""" - process_instance.status = ProcessInstanceStatus.suspended.value - db.session.add(process_instance) + self.process_instance_model.status = ProcessInstanceStatus.suspended.value + db.session.add(self.process_instance_model) db.session.commit() - @classmethod - def resume(cls, process_instance: ProcessInstanceModel) -> None: + def resume(self) -> None: """Resume.""" - process_instance.status = ProcessInstanceStatus.waiting.value - db.session.add(process_instance) + self.process_instance_model.status = ProcessInstanceStatus.waiting.value + db.session.add(self.process_instance_model) db.session.commit() diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py index 5e149965d..d7ea5613b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py @@ -25,6 +25,7 @@ from spiffworkflow_backend.models.process_instance_file_data import ( from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.task import Task from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.services.assertion_service import safe_assertion from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.git_service import GitCommandError from spiffworkflow_backend.services.git_service import GitService @@ -95,6 +96,13 @@ class ProcessInstanceService: ) process_instance_lock_prefix = "Background" for process_instance in records: + with safe_assertion(process_instance.status == status_value) as false_assumption: + if false_assumption: + raise AssertionError( + f"Queue assumed process instance {process_instance.id} has status of {status_value} " + f"when it really is {process_instance.status}" + ) + locked = False processor = None try: diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index 2feb2d596..858f2bcb1 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -1419,7 +1419,7 @@ class TestProcessApi(BaseTest): ) processor.save() - processor.suspend(process_instance) + processor.suspend() payload["description"] = "Message To Suspended" response = client.post( f"/v1.0/messages/{message_model_identifier}", @@ -1431,7 +1431,7 @@ class TestProcessApi(BaseTest): assert response.json assert response.json["error_code"] == "message_not_accepted" - processor.resume(process_instance) + processor.resume() payload["description"] = "Message To Resumed" response = client.post( f"/v1.0/messages/{message_model_identifier}",