bec-server 4.3.0__tar.gz → 4.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. {bec_server-4.3.0 → bec_server-4.3.1}/PKG-INFO +1 -1
  2. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/actors/worker.py +5 -15
  3. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/procedures/container_utils.py +3 -2
  4. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/procedures/container_worker.py +1 -1
  5. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/procedures/manager.py +32 -7
  6. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/procedures/oop_worker_base.py +22 -4
  7. bec_server-4.3.1/bec_server/procedures/subprocess_worker.py +144 -0
  8. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/procedures/worker_base.py +17 -6
  9. {bec_server-4.3.0 → bec_server-4.3.1}/pyproject.toml +1 -1
  10. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_actors/test_actors.py +27 -2
  11. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_container_utils.py +35 -0
  12. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_procedure_container_worker.py +119 -2
  13. bec_server-4.3.1/tests/tests_scan_server/test_procedure_subprocess_worker.py +121 -0
  14. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_procedures.py +95 -1
  15. bec_server-4.3.0/bec_server/procedures/subprocess_worker.py +0 -73
  16. {bec_server-4.3.0 → bec_server-4.3.1}/.gitignore +0 -0
  17. {bec_server-4.3.0 → bec_server-4.3.1}/.zed/debug.json +0 -0
  18. {bec_server-4.3.0 → bec_server-4.3.1}/README.md +0 -0
  19. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/__init__.py +0 -0
  20. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/actors/__init__.py +0 -0
  21. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/actors/actor.py +0 -0
  22. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/actors/builtin_actor_manager.py +0 -0
  23. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/actors/manager.py +0 -0
  24. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/actors/scan_interlock.py +0 -0
  25. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/bec_server_utils/__init__.py +0 -0
  26. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/bec_server_utils/launch.py +0 -0
  27. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/bec_server_utils/service_handler.py +0 -0
  28. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/bec_server_utils/subprocess_launch.py +0 -0
  29. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/bec_server_utils/tmux_launch.py +0 -0
  30. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/data_processing/__init__.py +0 -0
  31. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/data_processing/cli/__init__.py +0 -0
  32. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/data_processing/cli/launch.py +0 -0
  33. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/data_processing/dap_server.py +0 -0
  34. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/data_processing/dap_service.py +0 -0
  35. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/data_processing/dap_service_manager.py +0 -0
  36. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/data_processing/lmfit1d_service.py +0 -0
  37. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/device_server/__init__.py +0 -0
  38. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/device_server/bec_message_handler.py +0 -0
  39. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/device_server/cli/__init__.py +0 -0
  40. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/device_server/cli/launch.py +0 -0
  41. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/device_server/device_server.py +0 -0
  42. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/device_server/devices/__init__.py +0 -0
  43. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/device_server/devices/config_update_handler.py +0 -0
  44. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/device_server/devices/device_serializer.py +0 -0
  45. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/device_server/devices/devicemanager.py +0 -0
  46. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/device_server/friendly_device_exceptions.py +0 -0
  47. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/device_server/ophyd_callback_monitor.py +0 -0
  48. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/device_server/rpc_handler.py +0 -0
  49. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/device_server/tests/__init__.py +0 -0
  50. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/device_server/tests/utils.py +0 -0
  51. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/file_writer/__init__.py +0 -0
  52. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/file_writer/async_writer.py +0 -0
  53. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/file_writer/cli/__init__.py +0 -0
  54. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/file_writer/cli/launch.py +0 -0
  55. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/file_writer/default_writer.py +0 -0
  56. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/file_writer/file_writer.py +0 -0
  57. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/file_writer/file_writer_manager.py +0 -0
  58. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/file_writer/merged_dicts.py +0 -0
  59. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/procedures/Containerfile.bec_linux_base +0 -0
  60. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/procedures/Containerfile.requirements +0 -0
  61. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/procedures/Containerfile.worker +0 -0
  62. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/procedures/__init__.py +0 -0
  63. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/procedures/_dev_runner.py +0 -0
  64. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/procedures/builtin_procedures.py +0 -0
  65. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/procedures/constants.py +0 -0
  66. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/procedures/procedure_registry.py +0 -0
  67. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/procedures/protocol.py +0 -0
  68. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_bundler/__init__.py +0 -0
  69. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_bundler/bec_emitter.py +0 -0
  70. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_bundler/cli/__init__.py +0 -0
  71. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_bundler/cli/launch.py +0 -0
  72. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_bundler/emitter.py +0 -0
  73. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_bundler/scan_bundler.py +0 -0
  74. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/__init__.py +0 -0
  75. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/beamline_state_manager.py +0 -0
  76. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/cli/__init__.py +0 -0
  77. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/cli/launch.py +0 -0
  78. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/device_lock_registry.py +0 -0
  79. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/direct_scan_worker.py +0 -0
  80. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/errors.py +0 -0
  81. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/instruction_handler.py +0 -0
  82. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scan_assembler.py +0 -0
  83. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scan_guard.py +0 -0
  84. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scan_manager.py +0 -0
  85. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scan_queue.py +0 -0
  86. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scan_server.py +0 -0
  87. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scan_worker.py +0 -0
  88. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/__init__.py +0 -0
  89. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/acquire.py +0 -0
  90. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/cont_line_scan.py +0 -0
  91. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/device_rpc.py +0 -0
  92. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/fermat_scan.py +0 -0
  93. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/grid_scan.py +0 -0
  94. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/hexagonal_scan.py +0 -0
  95. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/line_scan.py +0 -0
  96. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/line_sweep_scan.py +0 -0
  97. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/list_scan.py +0 -0
  98. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/log_scan.py +0 -0
  99. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/move.py +0 -0
  100. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/multi_region_grid_scan.py +0 -0
  101. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/multi_region_line_scan.py +0 -0
  102. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/path_optimization.py +0 -0
  103. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/position_generators.py +0 -0
  104. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/round_roi_scan.py +0 -0
  105. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/round_scan.py +0 -0
  106. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/scan_actions.py +0 -0
  107. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/scan_argument_modifier.py +0 -0
  108. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/scan_base.py +0 -0
  109. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/scan_components.py +0 -0
  110. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/scan_modifier.py +0 -0
  111. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/scan_status.py +0 -0
  112. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/time_scan.py +0 -0
  113. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/scans/updated_move.py +0 -0
  114. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/tests/__init__.py +0 -0
  115. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/tests/fixtures.py +0 -0
  116. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/tests/scan_fixtures.py +0 -0
  117. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/tests/scan_hook_tests.py +0 -0
  118. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scan_server/tests/utils.py +0 -0
  119. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scihub/__init__.py +0 -0
  120. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scihub/atlas/__init__.py +0 -0
  121. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scihub/atlas/atlas_active_client_emitter.py +0 -0
  122. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scihub/atlas/atlas_connector.py +0 -0
  123. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scihub/atlas/atlas_forwarder.py +0 -0
  124. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scihub/atlas/atlas_metadata_handler.py +0 -0
  125. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scihub/atlas/atlas_plugin_repo_emitter.py +0 -0
  126. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scihub/atlas/config_handler.py +0 -0
  127. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scihub/cli/__init__.py +0 -0
  128. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scihub/cli/launch.py +0 -0
  129. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scihub/scihub.py +0 -0
  130. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scihub/service_handler/__init__.py +0 -0
  131. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/scihub/service_handler/service_handler.py +0 -0
  132. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/test/__init__.py +0 -0
  133. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/test/actor_test_utils.py +0 -0
  134. {bec_server-4.3.0 → bec_server-4.3.1}/bec_server/test/helpers.py +0 -0
  135. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_bec_server_utils/test_main.py +0 -0
  136. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_bec_server_utils/test_service_handler.py +0 -0
  137. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_bec_server_utils/test_subprocess_launch.py +0 -0
  138. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_bec_server_utils/test_tmux_launch.py +0 -0
  139. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_data_processing/conftest.py +0 -0
  140. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_data_processing/test_dap_cli_launch.py +0 -0
  141. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_data_processing/test_dap_server.py +0 -0
  142. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_data_processing/test_dap_service_manager.py +0 -0
  143. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_data_processing/test_lmfit1d_service.py +0 -0
  144. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_device_server/conftest.py +0 -0
  145. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_device_server/test_config_handler.py +0 -0
  146. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_device_server/test_device_instructions.py +0 -0
  147. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_device_server/test_device_manager_ds.py +0 -0
  148. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_device_server/test_device_serializer.py +0 -0
  149. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_device_server/test_device_server.py +0 -0
  150. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_device_server/test_device_server_cli_launch.py +0 -0
  151. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_device_server/test_ophyd_callback_monitor.py +0 -0
  152. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_device_server/test_rpc_handler.py +0 -0
  153. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_file_writer/conftest.py +0 -0
  154. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_file_writer/test_async_file_writer.py +0 -0
  155. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_file_writer/test_file_writer.py +0 -0
  156. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_file_writer/test_file_writer_cli_launch.py +0 -0
  157. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_file_writer/test_file_writer_manager.py +0 -0
  158. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_bundler/conftest.py +0 -0
  159. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_bundler/test_bec_emitter.py +0 -0
  160. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_bundler/test_scan_bundler.py +0 -0
  161. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_bundler/test_scan_bundler_cli_launch.py +0 -0
  162. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/conftest.py +0 -0
  163. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/__init__.py +0 -0
  164. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_acquire.py +0 -0
  165. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_cont_line_scan.py +0 -0
  166. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_device_rpc_scan.py +0 -0
  167. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_fermat_scan.py +0 -0
  168. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_grid_scan.py +0 -0
  169. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_hexagonal_scan.py +0 -0
  170. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_line_scan.py +0 -0
  171. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_line_sweep_scan.py +0 -0
  172. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_list_scan.py +0 -0
  173. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_log_scan.py +0 -0
  174. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_move_scan.py +0 -0
  175. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_multi_region_grid_scan.py +0 -0
  176. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_multi_region_line_scan.py +0 -0
  177. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_position_preview.py +0 -0
  178. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_relative_scan_return_to_start.py +0 -0
  179. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_round_roi_scan.py +0 -0
  180. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_round_scan.py +0 -0
  181. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_scan_actions.py +0 -0
  182. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_scan_components.py +0 -0
  183. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_scan_status.py +0 -0
  184. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_time_scan.py +0 -0
  185. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/scans/test_updated_move_scan.py +0 -0
  186. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_beamline_state_manager.py +0 -0
  187. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_builtin_actor_manager.py +0 -0
  188. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_device_lock_registry.py +0 -0
  189. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_direct_scan_worker.py +0 -0
  190. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_instruction_handler.py +0 -0
  191. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_path_optimization.py +0 -0
  192. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_scan_argument_modifier.py +0 -0
  193. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_scan_assembler.py +0 -0
  194. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_scan_fixture_imports.py +0 -0
  195. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_scan_guard.py +0 -0
  196. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_scan_interlock.py +0 -0
  197. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_scan_modifier.py +0 -0
  198. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_scan_server_cli_launch.py +0 -0
  199. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_scan_server_queue.py +0 -0
  200. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_scan_server_scan_manager.py +0 -0
  201. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scan_server/test_scan_worker.py +0 -0
  202. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scihub/conftest.py +0 -0
  203. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scihub/test_atlas_active_client_emitter.py +0 -0
  204. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scihub/test_atlas_config_handler.py +0 -0
  205. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scihub/test_atlas_connector.py +0 -0
  206. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scihub/test_atlas_forwarder.py +0 -0
  207. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scihub/test_atlas_metadata_handler.py +0 -0
  208. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scihub/test_atlas_plugin_repo_emitter.py +0 -0
  209. {bec_server-4.3.0 → bec_server-4.3.1}/tests/tests_scihub/test_scihub_cli_launch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bec-server
3
- Version: 4.3.0
3
+ Version: 4.3.1
4
4
  Summary: BEC server
5
5
  Project-URL: Bug Tracker, https://github.com/bec-project/bec/issues
6
6
  Project-URL: Homepage, https://github.com/bec-project/bec
@@ -1,14 +1,12 @@
1
1
  import importlib
2
2
  import os
3
- from contextlib import redirect_stdout
4
3
  from inspect import isclass
5
4
 
6
5
  from bec_lib.client import BECClient
7
- from bec_lib.logger import LogLevel, bec_logger
6
+ from bec_lib.logger import bec_logger
8
7
  from bec_lib.messages import ProcedureWorkerStatus
9
- from bec_lib.redis_connector import RedisConnector
10
8
  from bec_server.actors.actor import ActorBase
11
- from bec_server.procedures.oop_worker_base import RedisOutputDiverter, get_env, push_status, setup
9
+ from bec_server.procedures.oop_worker_base import get_env, push_status, setup
12
10
  from bec_server.procedures.subprocess_worker import SubProcessWorker
13
11
 
14
12
  logger = bec_logger.logger
@@ -53,20 +51,12 @@ def get_actor_env():
53
51
  def main():
54
52
  env, helper, client, conn = setup(get_env())
55
53
  actor_env = get_actor_env()
56
- logger_connector = RedisConnector(env["redis_server"])
57
- output_diverter = RedisOutputDiverter(logger_connector, env["queue"])
58
- with redirect_stdout(output_diverter):
59
- logger.add(
60
- output_diverter,
61
- level=LogLevel.SUCCESS,
62
- format=bec_logger.formatting(is_container=True),
63
- filter=bec_logger.filter(),
64
- )
65
- logger.success(f"Starting ActorProcedureWorker with env: {actor_env}")
54
+ # The parent SubProcessWorker streams stdout and stderr to Redis.
55
+ logger.success(f"Starting ActorProcedureWorker with env: {actor_env}")
66
56
  push_status(conn, env["queue"], ProcedureWorkerStatus.IDLE)
67
57
  actor_procedure(bec=client, **actor_env)
58
+ push_status(conn, env["queue"], ProcedureWorkerStatus.FINISHED)
68
59
  conn.shutdown()
69
- logger_connector.shutdown()
70
60
 
71
61
 
72
62
  if __name__ == "__main__":
@@ -220,7 +220,7 @@ class PodmanCliUtils(_PodmanUtilsBase):
220
220
  _name_arg = ["--replace", "--name", container_name] if container_name else []
221
221
  if container_name:
222
222
  # in case of incomplete cleanup previously, even with --replace it is possible to get an error
223
- _run_and_capture_error("podman", "rm", "-f", container_name)
223
+ _run_and_capture_error("podman", "rm", "-f", "--ignore", container_name)
224
224
  return (
225
225
  _run_and_capture_error(
226
226
  "podman",
@@ -251,7 +251,8 @@ class PodmanCliUtils(_PodmanUtilsBase):
251
251
 
252
252
  def logs(self, id: str) -> list[str]:
253
253
  try:
254
- return _run_and_capture_error("podman", "logs", id).stderr.decode().splitlines()
254
+ output = _run_and_capture_error("podman", "logs", id)
255
+ return (output.stdout + output.stderr).decode(errors="replace").splitlines()
255
256
  except ProcedureWorkerError as e:
256
257
  logger.error(e)
257
258
  return [f"No logs found for container {id}\n"]
@@ -28,7 +28,7 @@ class ContainerProcedureWorker(OutOfProcessWorkerBase):
28
28
  self._backend.build_worker_image()
29
29
  self._container_id = self._backend.run(
30
30
  image_tag,
31
- self._worker_environment(),
31
+ self._worker_environment() | self._additional_env,
32
32
  [
33
33
  {
34
34
  "source": str(PROCEDURE.CONTAINER.DEPLOYMENT_PATH),
@@ -46,6 +46,8 @@ class RedisCredentials(TypedDict):
46
46
 
47
47
  def _log_on_end(future: Future):
48
48
  """Use as a callback so that future is always done."""
49
+ if future.cancelled():
50
+ return
49
51
  if e := future.exception():
50
52
  logger.error(f"Worker failed with exception: {e}")
51
53
  else:
@@ -141,9 +143,9 @@ class ProcedureManagerBase(ABC, Generic[_ReqMsgT, _ExecMsgT]):
141
143
  with self.lock:
142
144
  if exec_msg.queue not in self._active_workers:
143
145
  new_worker = self.executor.submit(self.spawn, queue=queue, env=env)
146
+ self._active_workers[queue] = {"worker": None, "future": new_worker}
144
147
  new_worker.add_done_callback(_log_on_end)
145
148
  new_worker.add_done_callback(self._cleanup_worker_function(queue))
146
- self._active_workers[queue] = {"worker": None, "future": new_worker}
147
149
  return exec_msg
148
150
 
149
151
  def spawn(self, queue: str, env: dict[str, str]):
@@ -156,11 +158,20 @@ class ProcedureManagerBase(ABC, Generic[_ReqMsgT, _ExecMsgT]):
156
158
  raise WorkerAlreadyExists(
157
159
  f"Queue {queue} already has an active worker in {self._active_workers}!"
158
160
  )
159
- with self._worker_cls(self._server, queue, PROCEDURE.WORKER.QUEUE_TIMEOUT_S, env) as worker:
160
- with self.lock:
161
- self._active_workers[queue]["worker"] = worker
162
- worker.work()
163
- self._logs.extend(worker.logs())
161
+ worker = None
162
+ try:
163
+ with self._worker_cls(
164
+ self._server, queue, PROCEDURE.WORKER.QUEUE_TIMEOUT_S, env
165
+ ) as worker:
166
+ with self.lock:
167
+ self._active_workers[queue]["worker"] = worker
168
+ worker.work()
169
+ finally:
170
+ if worker is not None:
171
+ logs = worker.logs()
172
+ self._logs.extend(logs)
173
+ for line in logs:
174
+ logger.info(f"Procedure worker '{queue}': {line}")
164
175
 
165
176
  def _wait_for_all_futures(self):
166
177
  with self.lock:
@@ -195,7 +206,7 @@ class ProcedureManagerBase(ABC, Generic[_ReqMsgT, _ExecMsgT]):
195
206
  self._conn.shutdown()
196
207
  # cancel futures by hand to give us the opportunity to detach them from redis if they have started
197
208
  with self.lock:
198
- for entry in self._active_workers.values():
209
+ for entry in list(self._active_workers.values()):
199
210
  cancelled = entry["future"].cancel()
200
211
  if not cancelled:
201
212
  # unblock any waiting workers and let them shutdown
@@ -292,6 +303,20 @@ class ProcedureManager(ProcedureManagerBase[ProcedureRequestMessage, ProcedureEx
292
303
  def cleanup_worker(fut):
293
304
  with self.lock:
294
305
  logger.debug(f"cleaning up worker {fut} for queue {queue}...")
306
+ error = None if fut.cancelled() else fut.exception()
307
+ if error is not None:
308
+ pending = self._helper.get.exec_queue(queue)
309
+ running = [
310
+ msg for msg in self._helper.get.running_procedures() if msg.queue == queue
311
+ ]
312
+ self._helper.move.execution_queue_to_unhandled(queue)
313
+ for msg in running:
314
+ self._helper.push.unhandled(queue, msg)
315
+ for msg in pending + running:
316
+ self._helper.status_update(
317
+ msg.execution_id, "Finished", error=f"Procedure worker failed: {error}"
318
+ )
319
+ self._helper.notify_watchers(queue, "unhandled")
295
320
  self._helper.remove_from_active.by_queue(queue)
296
321
  self._run_callbacks(queue)
297
322
  self._helper.notify_watchers(queue, "execution")
@@ -59,7 +59,7 @@ class OutOfProcessWorkerBase(ProcedureWorker):
59
59
  minimum necessary, or things which are only necessary for the functioning of the worker,
60
60
  and other information should be passed through redis"""
61
61
  return {
62
- "redis_server": f"{PROCEDURE.REDIS_HOST}:{self._conn.port}",
62
+ "redis_server": self._redis_server,
63
63
  "queue": self._queue,
64
64
  "timeout_s": str(self._lifetime_s),
65
65
  }
@@ -70,7 +70,7 @@ class OutOfProcessWorkerBase(ProcedureWorker):
70
70
  # on timeout check if container is still running
71
71
 
72
72
  status_update = None
73
- while not self._ending_or_ended():
73
+ while not self._aborted.is_set():
74
74
  status_update = self._conn.blocking_list_pop(
75
75
  MessageEndpoints.procedure_worker_status_update(self._queue), timeout_s=0.2
76
76
  )
@@ -82,10 +82,17 @@ class OutOfProcessWorkerBase(ProcedureWorker):
82
82
  logger.info(
83
83
  f"Procedure worker '{self._queue}' status update: {status_update.status.name}"
84
84
  )
85
+ elif self._ending_or_ended():
86
+ break
85
87
  # TODO: we probably do want to handle some kind of timeout here but we don't know how
86
88
  # long a running procedure should actually take - it could theoretically be infinite
87
89
  if self.status != ProcedureWorkerStatus.FINISHED:
88
90
  self.status = ProcedureWorkerStatus.DEAD
91
+ if not self._aborted.is_set():
92
+ logs = "\n".join(self.logs())
93
+ raise ProcedureWorkerError(
94
+ f"Procedure worker for queue {self._queue} exited unexpectedly.\n{logs}"
95
+ )
89
96
 
90
97
 
91
98
  def _default_env() -> dict[str, str]:
@@ -230,10 +237,21 @@ def _main(
230
237
  helper.remove_from_active.by_exec_id(item.execution_id)
231
238
 
232
239
 
233
- def main():
234
- """Replaces the main contents of Worker.work() - should be called as the container entrypoint or command"""
240
+ def main(*, output_to_redis: bool = True) -> None:
241
+ """Run the worker entry point.
242
+
243
+ Args:
244
+ output_to_redis (bool): Redirect procedure output to Redis in this process. Subprocess
245
+ workers disable this because their parent streams both output pipes instead.
246
+ """
235
247
 
236
248
  env, helper, client, conn = setup(get_env())
249
+ if not output_to_redis:
250
+ try:
251
+ _main(env, helper, client, conn)
252
+ finally:
253
+ conn.shutdown()
254
+ return
237
255
  logger_connector = RedisConnector(env["redis_server"])
238
256
  output_diverter = RedisOutputDiverter(logger_connector, env["queue"])
239
257
  with redirect_stdout(output_diverter):
@@ -0,0 +1,144 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ from codecs import getincrementaldecoder
5
+ from collections import deque
6
+ from signal import SIGINT
7
+ from threading import Lock, Thread
8
+
9
+ from bec_lib.endpoints import MessageEndpoints
10
+ from bec_lib.logger import bec_logger
11
+ from bec_lib.messages import RawMessage
12
+ from bec_lib.redis_connector import RedisConnector
13
+ from bec_server.procedures.constants import ProcedureWorkerError
14
+ from bec_server.procedures.oop_worker_base import PROCESS_TIMEOUT, OutOfProcessWorkerBase, main
15
+
16
+ logger = bec_logger.logger
17
+
18
+
19
+ class SubProcessWorker(OutOfProcessWorkerBase):
20
+ """Run procedures in a subprocess, streaming stdout and stderr to Redis."""
21
+
22
+ WORKER_FILE = __file__
23
+
24
+ def _setup_execution_environment(self):
25
+ # Match the UTF-8 decoder in the parent even when the host uses another encoding.
26
+ env: dict[str, str] = {"PYTHONIOENCODING": "utf-8"} | self._worker_environment()
27
+ first_start = not hasattr(self, "_output_logs")
28
+ self._output_error_logged = False
29
+ if first_start:
30
+ self._output_logs: deque[str] = deque(maxlen=10000)
31
+ self._output_lock = Lock()
32
+ output_conn = RedisConnector(
33
+ [self._redis_server], name=f"ProcedureWorker-{self._queue} output"
34
+ )
35
+ try:
36
+ if first_start:
37
+ output_conn.delete(MessageEndpoints.procedure_logs(self._queue))
38
+ self._process = subprocess.Popen(
39
+ [sys.executable, "-u", self.WORKER_FILE],
40
+ stdout=subprocess.PIPE,
41
+ stderr=subprocess.STDOUT,
42
+ bufsize=0,
43
+ env=os.environ | env | self._additional_env,
44
+ )
45
+ except BaseException:
46
+ output_conn.shutdown()
47
+ raise
48
+ self._output_thread = Thread(
49
+ target=self._capture_output,
50
+ args=(self._process, output_conn),
51
+ name=f"ProcedureWorker-{self._queue} output",
52
+ daemon=True,
53
+ )
54
+ self._output_thread.start()
55
+ logger.success("Subprocess Procedure Worker started.")
56
+ self._ending: bool = False
57
+
58
+ def _capture_output(self, process: subprocess.Popen, conn: RedisConnector) -> None:
59
+ """Drain both streams even if Redis rejects an output write."""
60
+ decoder = getincrementaldecoder("utf-8")(errors="replace")
61
+ try:
62
+ with process.stdout as output:
63
+ while True:
64
+ chunk = output.read(4096)
65
+ if not chunk:
66
+ self._publish_output(decoder.decode(b"", final=True), conn)
67
+ break
68
+ self._publish_output(decoder.decode(chunk), conn)
69
+ finally:
70
+ conn.shutdown()
71
+
72
+ def _publish_output(self, text: str, conn: RedisConnector) -> None:
73
+ if not text:
74
+ return
75
+ with self._output_lock:
76
+ self._output_logs.append(text)
77
+ try:
78
+ conn.xadd(
79
+ MessageEndpoints.procedure_logs(self._queue),
80
+ {"data": RawMessage(data=text)},
81
+ max_size=10000,
82
+ expire=3600,
83
+ )
84
+ except Exception as exc:
85
+ if not self._output_error_logged:
86
+ logger.error(f"Failed to publish procedure output: {exc}")
87
+ self._output_error_logged = True
88
+
89
+ def _join_output(self) -> None:
90
+ if hasattr(self, "_output_thread"):
91
+ self._output_thread.join(timeout=PROCESS_TIMEOUT)
92
+ if self._output_thread.is_alive():
93
+ raise ProcedureWorkerError("Subprocess output reader failed to stop")
94
+
95
+ def _cleanup_execution_environment(self) -> None:
96
+ try:
97
+ super()._cleanup_execution_environment()
98
+ finally:
99
+ self._join_output()
100
+
101
+ def _ending_or_ended(self):
102
+ return self._ending or (self._process.poll() is not None)
103
+
104
+ def _kill_process(self):
105
+ """Attempts to end the process gently by first sending SIGINT, which is handled by the BECIpythonClient to
106
+ gracefully shut down, and end any scans it has started. See:
107
+ bec_ipython_client/main.py#L125 and bec_ipython_client/signals.py#L103
108
+ If the process fails to shut down naturally, kills it unceremoniously. This will leave the running scan running
109
+ on the ScanServer.
110
+ """
111
+ if not self._ending_or_ended():
112
+ self._ending = True
113
+ self._process.send_signal(SIGINT)
114
+ self._process.send_signal(SIGINT)
115
+ try:
116
+ self._process.wait(PROCESS_TIMEOUT)
117
+ except subprocess.TimeoutExpired:
118
+ logger.warning(
119
+ f"Subprocess Procedure Worker failed to exit in {PROCESS_TIMEOUT} s. Killing."
120
+ )
121
+ self._process.kill()
122
+ self._process.wait(PROCESS_TIMEOUT)
123
+
124
+ def abort_execution(self, execution_id: str):
125
+ """Abort the execution with the given id. Has no effect if the given ID is not the current job"""
126
+ if execution_id == self._current_execution_id:
127
+ self._kill_process()
128
+ self._helper.remove_from_active.by_exec_id(execution_id)
129
+ logger.info(
130
+ f"Aborting execution {execution_id}, restarting worker for queue: {self._queue}"
131
+ )
132
+ self._join_output()
133
+ self._setup_execution_environment()
134
+
135
+ def logs(self) -> list[str]:
136
+ """Return captured output without consuming it or waiting for a live process."""
137
+ if self._process.poll() is not None:
138
+ self._join_output()
139
+ with self._output_lock:
140
+ return "".join(self._output_logs).splitlines()
141
+
142
+
143
+ if __name__ == "__main__":
144
+ main(output_to_redis=False)
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from threading import Event
4
+ from threading import Event, Lock
5
5
 
6
6
  from bec_lib.endpoints import MessageEndpoints
7
7
  from bec_lib.logger import bec_logger
@@ -52,15 +52,27 @@ class ProcedureWorker(ABC):
52
52
  self.client_id = self._conn.client_id()
53
53
  self._current_execution_id: str | None = None
54
54
  self._aborted = Event()
55
+ self._shutdown_lock = Lock()
55
56
 
56
- self._setup_execution_environment()
57
+ try:
58
+ self._setup_execution_environment()
59
+ except BaseException:
60
+ self._conn.shutdown()
61
+ raise
57
62
 
58
63
  def __enter__(self):
59
64
  return self
60
65
 
61
66
  def __exit__(self, exc_type, exc_val, exc_tb):
62
- self._conn.shutdown()
63
- self._kill_process()
67
+ self._cleanup_execution_environment()
68
+
69
+ def _cleanup_execution_environment(self) -> None:
70
+ """Serialize abort and context teardown, always stopping the external process."""
71
+ with self._shutdown_lock:
72
+ try:
73
+ self._conn.shutdown()
74
+ finally:
75
+ self._kill_process()
64
76
 
65
77
  @abstractmethod
66
78
  def _kill_process(self):
@@ -86,8 +98,7 @@ class ProcedureWorker(ABC):
86
98
  def abort(self):
87
99
  """Abort the entire worker"""
88
100
  self._aborted.set()
89
- self._conn.shutdown()
90
- self._kill_process()
101
+ self._cleanup_execution_environment()
91
102
 
92
103
  @abstractmethod
93
104
  def abort_execution(self, execution_id: str):
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bec-server"
7
- version = "4.3.0"
7
+ version = "4.3.1"
8
8
  description = "BEC server"
9
9
  requires-python = ">=3.11"
10
10
  classifiers = [
@@ -20,8 +20,8 @@ from bec_server.test.helpers import wait_until
20
20
 
21
21
  @pytest.fixture
22
22
  def fakeredis_config():
23
- redis_config = "localhost", 44556
24
- server = TcpFakeServer(redis_config)
23
+ server = TcpFakeServer(("127.0.0.1", 0))
24
+ redis_config = server.server_address
25
25
  t = Thread(target=server.serve_forever, kwargs={"poll_interval": 0.1}, daemon=True)
26
26
  t.start()
27
27
  yield redis_config
@@ -122,6 +122,7 @@ def test_subscription_actor_unsubs():
122
122
  def test_polling_actor_inline(fakeredis_config):
123
123
  host, port = fakeredis_config
124
124
  redis = {"host": host, "port": port}
125
+ BECClient._reset_singleton()
125
126
  client: BECClient = _create_client(BecClientType.BECClient, redis=redis) # type: ignore
126
127
  client.start()
127
128
  redis = f"{host}:{port}"
@@ -140,6 +141,7 @@ def test_polling_actor_inline(fakeredis_config):
140
141
  actor.stop_event.set()
141
142
  actor_thread.join()
142
143
  client.shutdown()
144
+ BECClient._reset_singleton()
143
145
 
144
146
 
145
147
  def test_actor_procedure_happy_path():
@@ -263,3 +265,26 @@ def test_bl_state_actor_cancelled_startup(ready, stop_before_run, threads_check)
263
265
  mock_client.beamline_states.ready = True
264
266
  actor.stop_event.set()
265
267
  t.join(timeout=2)
268
+
269
+
270
+ @patch("bec_server.actors.worker.actor_procedure")
271
+ @patch("bec_server.actors.worker.push_status")
272
+ @patch("bec_server.actors.worker.setup")
273
+ @patch("bec_server.actors.worker.get_actor_env", return_value={})
274
+ @patch("bec_server.actors.worker.get_env")
275
+ def test_actor_worker_reports_finished(
276
+ get_env_mock, actor_env_mock, setup_mock, push_mock, procedure_mock
277
+ ):
278
+ from bec_server.actors.worker import main
279
+
280
+ conn = MagicMock()
281
+ setup_mock.return_value = (
282
+ {"redis_server": "localhost:6379", "queue": "test"},
283
+ None,
284
+ MagicMock(),
285
+ conn,
286
+ )
287
+ with patch("bec_server.actors.worker.logger"):
288
+ main()
289
+ assert push_mock.call_args.args == (conn, "test", ProcedureWorkerStatus.FINISHED)
290
+ conn.shutdown.assert_called_once()
@@ -1,3 +1,4 @@
1
+ from subprocess import CompletedProcess
1
2
  from unittest.mock import ANY, MagicMock, patch
2
3
 
3
4
  import pytest
@@ -217,3 +218,37 @@ def test_cli_get_state(cli_utils_with_fake_container_json: PodmanCliUtils):
217
218
  assert cli_utils_with_fake_container_json.state(
218
219
  "13826d25a737b733a5d87975b50a9c1efb5d087365d956cb7db2ccbe6aca07c4"
219
220
  ) == PodmanContainerStates("running")
221
+
222
+
223
+ def test_cli_logs_include_stdout_and_stderr(cli_utils):
224
+ utils, run = cli_utils
225
+ run.return_value.stdout = b"startup output\n"
226
+ run.return_value.stderr = b"startup error\n"
227
+ assert utils.logs("test") == ["startup output", "startup error"]
228
+
229
+
230
+ def test_cli_run_ignores_missing_previous_container(cli_utils):
231
+ utils, run_mock = cli_utils
232
+ run_mock.reset_mock()
233
+
234
+ def run_command(command, **kwargs):
235
+ missing_container = command[1] == "rm" and "--ignore" not in command
236
+ return CompletedProcess(
237
+ command,
238
+ returncode=1 if missing_container else 0,
239
+ stdout=b"worker-id\n",
240
+ stderr=b"No such container" if missing_container else b"",
241
+ )
242
+
243
+ run_mock.side_effect = run_command
244
+ container_id = utils.run(
245
+ "worker-image",
246
+ {"redis_server": "localhost:6379", "queue": "test", "timeout_s": "10"},
247
+ [],
248
+ "bec-procedure-worker",
249
+ container_name="new-worker",
250
+ )
251
+
252
+ assert container_id == "worker-id"
253
+ run_mock.assert_any_call(["podman", "rm", "-f", "--ignore", "new-worker"], capture_output=True)
254
+ assert run_mock.call_args.args[0][:2] == ["podman", "run"]
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import time
3
3
  from itertools import repeat
4
- from threading import Thread
4
+ from threading import Event, Thread
5
5
  from time import sleep
6
6
  from unittest.mock import MagicMock, call, patch
7
7
 
@@ -25,7 +25,7 @@ def test_container_worker_init(redis_mock):
25
25
  redis_mock().port = "port"
26
26
  worker = ContainerProcedureWorker(server="server:port", queue="test_queue", lifetime_s=1)
27
27
  assert worker._worker_environment() == {
28
- "redis_server": "redis:port",
28
+ "redis_server": "server:port",
29
29
  "queue": "test_queue",
30
30
  "timeout_s": "1",
31
31
  }
@@ -185,3 +185,120 @@ def test_main_running_newly_registered_proc(redis_mock, logger_mock):
185
185
  logger_mock.debug.assert_has_calls([call(f"running task {PROC_NAME}")], any_order=True)
186
186
  function_recorder.assert_called_once_with("a", b="c")
187
187
  logger_mock.success.assert_called_with("Procedure runner shutting down")
188
+
189
+
190
+ @patch("bec_server.procedures.container_worker.get_backend")
191
+ @patch("bec_server.procedures.worker_base.RedisConnector")
192
+ def test_container_worker_uses_supplied_environment(redis_mock, backend_mock):
193
+ worker = ContainerProcedureWorker(
194
+ server="127.0.0.1:20772",
195
+ queue="test",
196
+ env={"redis_server": "host.containers.internal:20772"},
197
+ )
198
+ assert backend_mock().run.call_args.args[1]["redis_server"] == "host.containers.internal:20772"
199
+ assert worker._worker_environment()["redis_server"] == "127.0.0.1:20772"
200
+
201
+
202
+ @patch("bec_server.procedures.container_worker.get_backend")
203
+ @patch("bec_server.procedures.worker_base.RedisConnector")
204
+ def test_container_worker_setup_failure_closes_connector(redis_mock, backend_mock):
205
+ backend_mock().run.side_effect = RuntimeError("missing pod")
206
+ with pytest.raises(RuntimeError, match="missing pod"):
207
+ ContainerProcedureWorker(server="localhost:6379", queue="test")
208
+ redis_mock().shutdown.assert_called_once()
209
+
210
+
211
+ @patch("bec_server.procedures.container_worker.get_backend")
212
+ @patch("bec_server.procedures.worker_base.RedisConnector")
213
+ def test_container_worker_early_exit_reports_logs(redis_mock, backend_mock):
214
+ from bec_server.procedures.constants import ProcedureWorkerError
215
+
216
+ redis_mock().blocking_list_pop.return_value = None
217
+ backend_mock().state.return_value = PodmanContainerStates.EXITED
218
+ backend_mock().logs.return_value = ["Cannot connect to Redis"]
219
+ worker = ContainerProcedureWorker(server="localhost:6379", queue="test")
220
+ with pytest.raises(ProcedureWorkerError, match="Cannot connect to Redis"):
221
+ worker.work()
222
+ assert worker.status == ProcedureWorkerStatus.DEAD
223
+
224
+
225
+ @patch("bec_server.procedures.container_worker.get_backend")
226
+ @patch("bec_server.procedures.worker_base.RedisConnector")
227
+ def test_container_worker_drains_final_status_before_exit(redis_mock, backend_mock):
228
+ redis_mock().blocking_list_pop.side_effect = [
229
+ ProcedureWorkerStatusMessage(worker_queue="test", status=ProcedureWorkerStatus.FINISHED),
230
+ None,
231
+ ]
232
+ backend_mock().state.return_value = PodmanContainerStates.EXITED
233
+ worker = ContainerProcedureWorker(server="localhost:6379", queue="test")
234
+ worker.work()
235
+ assert worker.status == ProcedureWorkerStatus.FINISHED
236
+
237
+
238
+ @patch("bec_server.procedures.container_worker.get_backend")
239
+ @patch("bec_server.procedures.worker_base.RedisConnector")
240
+ def test_abort_and_context_exit_serialize_connector_shutdown(redis_mock, backend_mock):
241
+ worker = ContainerProcedureWorker(server="localhost:6379", queue="test")
242
+ closing = Event()
243
+ release = Event()
244
+ exit_started = Event()
245
+ concurrent_close = Event()
246
+ errors = []
247
+
248
+ def close_connector():
249
+ if closing.is_set():
250
+ concurrent_close.set()
251
+ raise RuntimeError("concurrent Redis connector shutdown")
252
+ closing.set()
253
+ try:
254
+ assert release.wait(2)
255
+ finally:
256
+ closing.clear()
257
+
258
+ def abort():
259
+ try:
260
+ worker.abort()
261
+ except BaseException as exc:
262
+ errors.append(exc)
263
+
264
+ def exit_context():
265
+ exit_started.set()
266
+ try:
267
+ worker.__exit__(None, None, None)
268
+ except BaseException as exc:
269
+ errors.append(exc)
270
+
271
+ redis_mock().shutdown.side_effect = close_connector
272
+ worker._kill_process = MagicMock()
273
+ abort_thread = Thread(target=abort)
274
+ exit_thread = Thread(target=exit_context)
275
+ abort_thread.start()
276
+ try:
277
+ assert closing.wait(2)
278
+ exit_thread.start()
279
+ assert exit_started.wait(2)
280
+ assert not concurrent_close.wait(0.1)
281
+ finally:
282
+ release.set()
283
+ abort_thread.join(2)
284
+ if exit_thread.ident is not None:
285
+ exit_thread.join(2)
286
+ assert not abort_thread.is_alive()
287
+ assert not exit_thread.is_alive()
288
+ assert errors == []
289
+ worker._kill_process.assert_called()
290
+
291
+
292
+ @patch("bec_server.procedures.container_worker.get_backend")
293
+ @patch("bec_server.procedures.worker_base.RedisConnector")
294
+ @pytest.mark.parametrize("abort", [True, False])
295
+ def test_connector_shutdown_failure_still_stops_worker(redis_mock, backend_mock, abort):
296
+ worker = ContainerProcedureWorker(server="localhost:6379", queue="test")
297
+ redis_mock().shutdown.side_effect = RuntimeError("failed to close Redis")
298
+ worker._kill_process = MagicMock()
299
+ with pytest.raises(RuntimeError, match="failed to close Redis"):
300
+ if abort:
301
+ worker.abort()
302
+ else:
303
+ worker.__exit__(None, None, None)
304
+ worker._kill_process.assert_called_once()