pg-workload 0.6.1__tar.gz → 0.6.2__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 (141) hide show
  1. {pg_workload-0.6.1/src/pg_workload.egg-info → pg_workload-0.6.2}/PKG-INFO +1 -1
  2. {pg_workload-0.6.1 → pg_workload-0.6.2}/pyproject.toml +1 -1
  3. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/__init__.py +1 -1
  4. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/assets.py +3 -0
  5. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/cli.py +16 -1
  6. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/control.py +28 -1
  7. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/orchestration.py +3 -1
  8. {pg_workload-0.6.1 → pg_workload-0.6.2/src/pg_workload.egg-info}/PKG-INFO +1 -1
  9. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload.egg-info/SOURCES.txt +1 -0
  10. pg_workload-0.6.2/tests/test_scheduler_identity.py +57 -0
  11. {pg_workload-0.6.1 → pg_workload-0.6.2}/tests/test_workload.py +49 -3
  12. {pg_workload-0.6.1 → pg_workload-0.6.2}/LICENSE +0 -0
  13. {pg_workload-0.6.1 → pg_workload-0.6.2}/MANIFEST.in +0 -0
  14. {pg_workload-0.6.1 → pg_workload-0.6.2}/README.md +0 -0
  15. {pg_workload-0.6.1 → pg_workload-0.6.2}/THIRD_PARTY_NOTICES.md +0 -0
  16. {pg_workload-0.6.1 → pg_workload-0.6.2}/setup.cfg +0 -0
  17. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/__main__.py +0 -0
  18. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/bloat_vacuum/README.md +0 -0
  19. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/bloat_vacuum/generator.py +0 -0
  20. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/bloat_vacuum/profile.yml +0 -0
  21. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/bloat_vacuum/sql/01_churn.sql +0 -0
  22. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/bloat_vacuum/sql/accounts-schema.sql +0 -0
  23. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/emulate_errors/README.md +0 -0
  24. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/emulate_errors/profile.yml +0 -0
  25. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/emulate_errors/sql/01_workload.sql +0 -0
  26. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/emulate_errors/sql/schema.sql +0 -0
  27. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/README.md +0 -0
  28. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/generator.py +0 -0
  29. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/initialization-constraints.json +0 -0
  30. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/initialization-indexes.json +0 -0
  31. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/profile.yml +0 -0
  32. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/01_company_catalog.sql +0 -0
  33. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/02_people_by_keyword.sql +0 -0
  34. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/03_keyword_trends.sql +0 -0
  35. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/04_genre_cast.sql +0 -0
  36. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/05_join_stress.sql +0 -0
  37. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/imdb-schema.sql +0 -0
  38. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/initialization-finalize.sql +0 -0
  39. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_1.sql +0 -0
  40. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_10.sql +0 -0
  41. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_11.sql +0 -0
  42. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_12.sql +0 -0
  43. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_13.sql +0 -0
  44. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_14.sql +0 -0
  45. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_15.sql +0 -0
  46. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_16.sql +0 -0
  47. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_17.sql +0 -0
  48. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_18.sql +0 -0
  49. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_19.sql +0 -0
  50. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_2.sql +0 -0
  51. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_20.sql +0 -0
  52. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_21.sql +0 -0
  53. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_22.sql +0 -0
  54. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_23.sql +0 -0
  55. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_24.sql +0 -0
  56. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_25.sql +0 -0
  57. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_26.sql +0 -0
  58. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_27.sql +0 -0
  59. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_28.sql +0 -0
  60. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_29.sql +0 -0
  61. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_3.sql +0 -0
  62. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_30.sql +0 -0
  63. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_31.sql +0 -0
  64. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_32.sql +0 -0
  65. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_33.sql +0 -0
  66. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_4.sql +0 -0
  67. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_5.sql +0 -0
  68. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_6.sql +0 -0
  69. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_7.sql +0 -0
  70. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_8.sql +0 -0
  71. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/imdb/sql/select_9.sql +0 -0
  72. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/jsonb_docs/README.md +0 -0
  73. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/jsonb_docs/generator.py +0 -0
  74. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/jsonb_docs/profile.yml +0 -0
  75. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/jsonb_docs/sql/01_write.sql +0 -0
  76. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/jsonb_docs/sql/02_read.sql +0 -0
  77. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/jsonb_docs/sql/docs-schema.sql +0 -0
  78. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/many_objects/README.md +0 -0
  79. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/many_objects/profile.yml +0 -0
  80. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/many_objects/sql/01_workload.sql +0 -0
  81. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/many_objects/sql/prepare_schema_01.sql +0 -0
  82. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/many_objects/sql/prepare_schema_02.sql +0 -0
  83. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pagila/README.md +0 -0
  84. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pagila/generator.py +0 -0
  85. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pagila/initialization-constraints.json +0 -0
  86. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pagila/initialization-indexes.json +0 -0
  87. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pagila/profile.yml +0 -0
  88. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pagila/sql/01_select.sql +0 -0
  89. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pagila/sql/02_insert.sql +0 -0
  90. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pagila/sql/03_update.sql +0 -0
  91. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pagila/sql/04_delete.sql +0 -0
  92. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pagila/sql/initialization-finalize.sql +0 -0
  93. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pagila/sql/pagila-schema.sql +0 -0
  94. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/partition_aging/README.md +0 -0
  95. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/partition_aging/generator.py +0 -0
  96. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/partition_aging/profile.yml +0 -0
  97. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/partition_aging/sql/01_ingest.sql +0 -0
  98. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/partition_aging/sql/02_read_recent.sql +0 -0
  99. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/partition_aging/sql/03_read_history.sql +0 -0
  100. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/partition_aging/sql/events-schema.sql +0 -0
  101. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pss_overflow/README.md +0 -0
  102. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pss_overflow/generator.py +0 -0
  103. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pss_overflow/profile.yml +0 -0
  104. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pss_overflow/sql/01_workload.sql +0 -0
  105. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/pss_overflow/sql/pss_overflow-schema.sql +0 -0
  106. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/queue_skip_locked/README.md +0 -0
  107. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/queue_skip_locked/generator.py +0 -0
  108. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/queue_skip_locked/profile.yml +0 -0
  109. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/queue_skip_locked/sql/01_enqueue.sql +0 -0
  110. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/queue_skip_locked/sql/02_dequeue.sql +0 -0
  111. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/queue_skip_locked/sql/03_settle.sql +0 -0
  112. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/queue_skip_locked/sql/queue-schema.sql +0 -0
  113. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/simple_stock/README.md +0 -0
  114. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/simple_stock/generator.py +0 -0
  115. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/simple_stock/profile.yml +0 -0
  116. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/simple_stock/sql/01_workload.sql +0 -0
  117. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/simple_stock/sql/indexes.sql +0 -0
  118. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/simple_stock/sql/stock-schema.sql +0 -0
  119. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/simple_stock_spec_symbols/README.md +0 -0
  120. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/simple_stock_spec_symbols/generator.py +0 -0
  121. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/simple_stock_spec_symbols/profile.yml +0 -0
  122. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/simple_stock_spec_symbols/sql/01_workload.sql +0 -0
  123. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/simple_stock_spec_symbols/sql/indexes.sql +0 -0
  124. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/profiles/simple_stock_spec_symbols/sql/stock-schema.sql +0 -0
  125. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/bundled/schema/pg_workload-v1.schema.json +0 -0
  126. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/common.py +0 -0
  127. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/config.py +0 -0
  128. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/initialization.py +0 -0
  129. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/logging.py +0 -0
  130. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/pg_client.py +0 -0
  131. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/profiles.py +0 -0
  132. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/resources.py +0 -0
  133. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/runner.py +0 -0
  134. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/scheduler.py +0 -0
  135. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload/state.py +0 -0
  136. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload.egg-info/dependency_links.txt +0 -0
  137. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload.egg-info/entry_points.txt +0 -0
  138. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload.egg-info/requires.txt +0 -0
  139. {pg_workload-0.6.1 → pg_workload-0.6.2}/src/pg_workload.egg-info/top_level.txt +0 -0
  140. {pg_workload-0.6.1 → pg_workload-0.6.2}/tests/test_docker_postgres18.py +0 -0
  141. {pg_workload-0.6.1 → pg_workload-0.6.2}/tests/test_initialization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pg-workload
3
- Version: 0.6.1
3
+ Version: 0.6.2
4
4
  Summary: PostgreSQL workload emulator for diagnostic and observability testing
5
5
  Author-email: O2eg <oleg.ispu@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pg-workload"
7
- version = "0.6.1"
7
+ version = "0.6.2"
8
8
  description = "PostgreSQL workload emulator for diagnostic and observability testing"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  """PostgreSQL workload generation and scheduling tools."""
2
2
 
3
- __version__ = "0.6.1"
3
+ __version__ = "0.6.2"
4
4
 
5
5
  from pg_workload.assets import bundled_profiles_root, initialize_project
6
6
  from pg_workload.cli import build_parser
@@ -50,6 +50,9 @@ def _copy_tree(source_root: Path, destination_root: Path, *, force: bool) -> dic
50
50
  destination_root.mkdir(parents=True, exist_ok=True)
51
51
  for source in sorted(source_root.rglob("*")):
52
52
  relative = source.relative_to(source_root)
53
+ # Bytecode caches are interpreter by-products of the installation, not assets.
54
+ if "__pycache__" in relative.parts:
55
+ continue
53
56
  destination = destination_root / relative
54
57
  if source.is_symlink():
55
58
  raise WorkloadError(f"Bundled asset must not be a symlink: {source}")
@@ -374,6 +374,11 @@ def build_parser() -> argparse.ArgumentParser:
374
374
  stop_cmd.add_argument("--root", default=DEFAULT_ROOT, help="Initialized project directory")
375
375
  stop_cmd.add_argument("--daemon-lock-file", default=DEFAULT_DAEMON_LOCK_FILE)
376
376
  stop_cmd.add_argument("--scheduler-log-file", default=DEFAULT_SCHEDULER_LOG_FILE)
377
+ stop_cmd.add_argument(
378
+ "--expected-identity",
379
+ type=_scheduler_identity_arg,
380
+ help="Require the recorded scheduler identity before signalling",
381
+ )
377
382
  stop_cmd.add_argument("--timeout", type=positive_float_arg, default=10.0)
378
383
 
379
384
  plan_cmd = subparsers.add_parser("plan", help="Build a deterministic execution plan without connecting")
@@ -460,8 +465,18 @@ def _verify_plan_hash(args: argparse.Namespace, operation: str) -> dict[str, obj
460
465
  return plan
461
466
 
462
467
 
468
+ def _scheduler_identity_arg(value: str) -> dict[str, object]:
469
+ identity = json.loads(value)
470
+ if not isinstance(identity, dict):
471
+ raise argparse.ArgumentTypeError("scheduler identity must be a JSON object")
472
+ return identity
473
+
474
+
463
475
  def _scheduler_paths(args: argparse.Namespace) -> tuple[Path, Path, Path]:
464
476
  root = Path(args.root).resolve()
477
+ # Without this a mistyped --root reads as "stopped" while the real scheduler keeps running.
478
+ if not root.is_dir():
479
+ raise WorkloadError(f"Workload project directory does not exist: {root}")
465
480
  lock_path = resolve_relative_path(root, args.daemon_lock_file, "scheduler lock file")
466
481
  log_path = resolve_relative_path(root, args.scheduler_log_file, "scheduler log file")
467
482
  return root, lock_path, log_path
@@ -593,7 +608,7 @@ def _run(args: argparse.Namespace) -> int:
593
608
  return 0
594
609
  if args.command == "stop":
595
610
  root, lock_path, log_path = _scheduler_paths(args)
596
- result = stop_scheduler(root, lock_path, log_path, args.timeout)
611
+ result = stop_scheduler(root, lock_path, log_path, args.timeout, expected_identity=args.expected_identity)
597
612
  if args.machine:
598
613
  _emit_machine(args, "succeeded", result=result)
599
614
  else:
@@ -46,6 +46,18 @@ def _owned(pid: int, root: Path) -> bool:
46
46
  )
47
47
 
48
48
 
49
+ def scheduler_identity(pid: int) -> dict[str, Any] | None:
50
+ try:
51
+ stat = Path(f"/proc/{pid}/stat").read_text().rsplit(")", 1)[1].split()
52
+ return {
53
+ "pid": pid,
54
+ "start_ticks": int(stat[19]),
55
+ "boot_id": Path("/proc/sys/kernel/random/boot_id").read_text().strip(),
56
+ }
57
+ except (OSError, ValueError, IndexError):
58
+ return None
59
+
60
+
49
61
  def scheduler_status(root: Path, lock_path: Path, log_path: Path) -> dict[str, Any]:
50
62
  pid = _pid(lock_path)
51
63
  running = pid is not None and _owned(pid, root)
@@ -55,6 +67,7 @@ def scheduler_status(root: Path, lock_path: Path, log_path: Path) -> dict[str, A
55
67
  "pid": pid if running else None,
56
68
  "stale_pid": pid if pid is not None and not running else None,
57
69
  "owned": running,
70
+ "identity": scheduler_identity(pid) if running else None,
58
71
  "root": str(root.resolve()),
59
72
  "lock_file": str(lock_path),
60
73
  "log_file": str(log_path),
@@ -139,6 +152,7 @@ def stop_scheduler(
139
152
  lock_path: Path,
140
153
  log_path: Path,
141
154
  timeout: float,
155
+ expected_identity: dict[str, Any] | None = None,
142
156
  ) -> dict[str, Any]:
143
157
  pid = _pid(lock_path)
144
158
  if pid is None:
@@ -148,7 +162,20 @@ def stop_scheduler(
148
162
  raise WorkloadOwnershipError(f"Refusing to signal pid {pid}: it is not a pg-workload scheduler for {root}")
149
163
  _clear_stale_pid(lock_path, pid)
150
164
  return scheduler_status(root, lock_path, log_path)
151
- os.kill(pid, signal.SIGTERM)
165
+ if expected_identity is not None:
166
+ # Pin the process before checking its identity: PID reuse must never redirect the signal.
167
+ try:
168
+ fd = os.pidfd_open(pid)
169
+ except ProcessLookupError:
170
+ return scheduler_status(root, lock_path, log_path)
171
+ try:
172
+ if scheduler_identity(pid) != expected_identity:
173
+ raise WorkloadOwnershipError("Scheduler identity differs from the recorded run; refusing to stop it")
174
+ signal.pidfd_send_signal(fd, signal.SIGTERM)
175
+ finally:
176
+ os.close(fd)
177
+ else:
178
+ os.kill(pid, signal.SIGTERM)
152
179
  deadline = time.monotonic() + timeout
153
180
  while time.monotonic() < deadline:
154
181
  if not _cmdline(pid):
@@ -54,7 +54,9 @@ def _file_hash(path: Path) -> str:
54
54
  def profile_descriptor(profile: Profile) -> dict[str, Any]:
55
55
  files = []
56
56
  for path in sorted(profile.path.rglob("*")):
57
- if not path.is_file() or "log" in path.relative_to(profile.path).parts:
57
+ parts = path.relative_to(profile.path).parts
58
+ # Job logs and interpreter bytecode caches are run by-products, not profile content.
59
+ if not path.is_file() or "log" in parts or "__pycache__" in parts:
58
60
  continue
59
61
  files.append(
60
62
  {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pg-workload
3
- Version: 0.6.1
3
+ Version: 0.6.2
4
4
  Summary: PostgreSQL workload emulator for diagnostic and observability testing
5
5
  Author-email: O2eg <oleg.ispu@gmail.com>
6
6
  License-Expression: MIT
@@ -135,4 +135,5 @@ src/pg_workload/bundled/profiles/simple_stock_spec_symbols/sql/stock-schema.sql
135
135
  src/pg_workload/bundled/schema/pg_workload-v1.schema.json
136
136
  tests/test_docker_postgres18.py
137
137
  tests/test_initialization.py
138
+ tests/test_scheduler_identity.py
138
139
  tests/test_workload.py
@@ -0,0 +1,57 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ import time
5
+
6
+ import pytest
7
+
8
+ from pg_workload.control import WorkloadOwnershipError, scheduler_status, stop_scheduler
9
+
10
+
11
+ def test_guarded_stop_leaves_replacement_scheduler_alive(tmp_path):
12
+ (tmp_path / "data").mkdir()
13
+ lock = tmp_path / "state/scheduler.lock"
14
+ log = tmp_path / "scheduler.log"
15
+ with log.open("w") as output:
16
+ process = subprocess.Popen(
17
+ [
18
+ sys.executable,
19
+ "-m",
20
+ "pg_workload",
21
+ "scheduler",
22
+ "--root",
23
+ str(tmp_path),
24
+ "--target=external",
25
+ "--host=127.0.0.1",
26
+ "--dry-run",
27
+ ],
28
+ stdout=output,
29
+ stderr=output,
30
+ )
31
+ try:
32
+ deadline = time.monotonic() + 10
33
+ while time.monotonic() < deadline:
34
+ status = scheduler_status(tmp_path, lock, log)
35
+ if status["running"]:
36
+ break
37
+ assert process.poll() is None, log.read_text()
38
+ time.sleep(0.05)
39
+ assert status["running"], log.read_text()
40
+ identity = status["identity"]
41
+ assert identity["pid"] == process.pid
42
+ for old in [
43
+ {},
44
+ dict(identity, start_ticks=identity["start_ticks"] - 1),
45
+ dict(identity, boot_id="old-boot"),
46
+ dict(identity, pid=os.getpid()),
47
+ ]:
48
+ with pytest.raises(WorkloadOwnershipError, match="identity differs"):
49
+ stop_scheduler(tmp_path, lock, log, 3, expected_identity=old)
50
+ assert process.poll() is None
51
+ stopped = stop_scheduler(tmp_path, lock, log, 5, expected_identity=identity)
52
+ assert not stopped["running"]
53
+ process.wait(timeout=5)
54
+ finally:
55
+ if process.poll() is None:
56
+ process.terminate()
57
+ process.wait(timeout=5)
@@ -4,10 +4,12 @@ import dataclasses
4
4
  import io
5
5
  import json
6
6
  import os
7
+ import queue
7
8
  import re
8
9
  import subprocess
9
10
  import sys
10
11
  import tempfile
12
+ import threading
11
13
  import time
12
14
  import types
13
15
  import unittest
@@ -157,6 +159,10 @@ class WorkloadGeneratorTests(unittest.TestCase):
157
159
  first = plan_hash()
158
160
  self.assertEqual(first, plan_hash())
159
161
  self.assertNotEqual(first, plan_hash(disk_max_used_pct=95))
162
+ cache = root / "data" / "pagila" / "__pycache__"
163
+ cache.mkdir(exist_ok=True)
164
+ (cache / "generator.cpython-399.pyc").write_bytes(b"interpreter by-product")
165
+ self.assertEqual(first, plan_hash())
160
166
  profile = root / "data" / "pagila" / "profile.yml"
161
167
  profile.write_text(
162
168
  profile.read_text(encoding="utf-8") + "\n# local experiment\n",
@@ -164,6 +170,19 @@ class WorkloadGeneratorTests(unittest.TestCase):
164
170
  )
165
171
  self.assertNotEqual(first, plan_hash())
166
172
 
173
+ def test_scheduler_commands_reject_a_missing_project_root(self):
174
+ with tempfile.TemporaryDirectory() as tmpdir:
175
+ missing = Path(tmpdir) / "no-such-project"
176
+ for command in ("status", "stop"):
177
+ stdout = io.StringIO()
178
+ with contextlib.redirect_stdout(stdout):
179
+ result = main(["--machine", command, "--root", str(missing)])
180
+ envelope = json.loads(stdout.getvalue())
181
+ self.assertNotEqual(0, result)
182
+ self.assertEqual("failed", envelope["status"])
183
+ self.assertIn("does not exist", envelope["error"]["message"])
184
+ self.assertFalse(missing.exists())
185
+
167
186
  def test_detached_scheduler_has_owned_start_status_stop_lifecycle(self):
168
187
  with tempfile.TemporaryDirectory() as tmpdir:
169
188
  root = Path(tmpdir)
@@ -764,6 +783,7 @@ class WorkloadGeneratorTests(unittest.TestCase):
764
783
  second = self.workload.initialize_project(root)
765
784
  self.assertGreater(first["assets"]["created"], 0)
766
785
  self.assertGreater(second["assets"]["unchanged"], 0)
786
+ self.assertEqual([], sorted(root.rglob("__pycache__")))
767
787
  self.assertTrue((root / "schema" / "pg_workload-v1.schema.json").is_file())
768
788
  manifest = root / "data" / "simple_stock" / "profile.yml"
769
789
  manifest.write_text(manifest.read_text(encoding="utf-8") + "# local edit\n", encoding="utf-8")
@@ -1379,8 +1399,30 @@ class WorkloadGeneratorTests(unittest.TestCase):
1379
1399
  stderr=subprocess.PIPE,
1380
1400
  text=True,
1381
1401
  )
1402
+ lines = []
1403
+ events = queue.Queue()
1404
+
1405
+ def read_stdout():
1406
+ for line in proc.stdout:
1407
+ lines.append(line)
1408
+ events.put(line)
1409
+
1410
+ reader = threading.Thread(target=read_stdout, daemon=True)
1411
+ reader.start()
1412
+
1413
+ def wait_for(text):
1414
+ deadline = time.monotonic() + 15
1415
+ while time.monotonic() < deadline:
1416
+ try:
1417
+ line = events.get(timeout=max(0.01, deadline - time.monotonic()))
1418
+ except queue.Empty:
1419
+ break
1420
+ if text in line:
1421
+ return
1422
+ self.fail(f"Timed out waiting for {text!r}: {''.join(lines)}")
1423
+
1382
1424
  try:
1383
- time.sleep(0.2)
1425
+ wait_for("Scheduler started.")
1384
1426
  self.create_minimal_profile(root, "dynamic_profile")
1385
1427
  self.workload.update_job_state(
1386
1428
  root / "state" / "workloads.yml",
@@ -1389,10 +1431,14 @@ class WorkloadGeneratorTests(unittest.TestCase):
1389
1431
  enabled=True,
1390
1432
  interval=60,
1391
1433
  )
1392
- time.sleep(0.5)
1434
+ wait_for("Running dynamic_profile:main")
1435
+ wait_for("pgbench")
1393
1436
  finally:
1394
1437
  proc.terminate()
1395
- stdout, stderr = proc.communicate(timeout=10)
1438
+ proc.wait(timeout=10)
1439
+ reader.join(timeout=5)
1440
+ stdout = "".join(lines)
1441
+ stderr = proc.stderr.read()
1396
1442
 
1397
1443
  self.assertIn("Scheduler started.", stdout)
1398
1444
  self.assertIn("Running dynamic_profile:main", stdout)
File without changes
File without changes
File without changes
File without changes