pgtask 0.2.0__tar.gz → 0.4.0__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 (86) hide show
  1. {pgtask-0.2.0 → pgtask-0.4.0}/Cargo.lock +143 -11
  2. {pgtask-0.2.0 → pgtask-0.4.0}/Cargo.toml +3 -1
  3. {pgtask-0.2.0 → pgtask-0.4.0}/PKG-INFO +1 -1
  4. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask/Cargo.toml +4 -4
  5. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-core/Cargo.toml +3 -0
  6. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-core/src/lib.rs +1 -1
  7. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-core/src/schedule.rs +15 -10
  8. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-core/src/task.rs +1 -0
  9. pgtask-0.4.0/crates/pgtask-core/tests/.gitignore +4 -0
  10. pgtask-0.4.0/crates/pgtask-core/tests/retry_properties.rs +124 -0
  11. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-core/tests/schedule_clock.rs +10 -9
  12. pgtask-0.4.0/crates/pgtask-core/tests/schedule_properties.rs +231 -0
  13. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-otel/src/lib.rs +3 -3
  14. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-otel/src/metrics.rs +15 -4
  15. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-postgres/Cargo.toml +6 -2
  16. pgtask-0.4.0/crates/pgtask-postgres/migrations/0003_remove_redundant_notifications.sql +63 -0
  17. pgtask-0.4.0/crates/pgtask-postgres/migrations/0004_sample_queue_demand.sql +176 -0
  18. pgtask-0.4.0/crates/pgtask-postgres/migrations/0005_serialise_result_wait_registration.sql +164 -0
  19. pgtask-0.4.0/crates/pgtask-postgres/migrations/0006_keep_resumed_tasks_claimable.sql +91 -0
  20. pgtask-0.4.0/crates/pgtask-postgres/migrations/0007_backfill_failed_attempts.sql +11 -0
  21. pgtask-0.4.0/crates/pgtask-postgres/migrations/0008_index_worker_pagination.sql +1 -0
  22. pgtask-0.4.0/crates/pgtask-postgres/migrations/0010_batch_task_transitions.sql +189 -0
  23. pgtask-0.4.0/crates/pgtask-postgres/migrations/0011_null_idle_delays.sql +79 -0
  24. pgtask-0.4.0/crates/pgtask-postgres/migrations/0013_resolve_waits_of_finished_tasks.sql +81 -0
  25. pgtask-0.4.0/crates/pgtask-postgres/migrations/0014_retention_throughput.sql +139 -0
  26. pgtask-0.4.0/crates/pgtask-postgres/migrations/0015_release_tasks.sql +104 -0
  27. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-postgres/src/lib.rs +2 -2
  28. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-postgres/src/store.rs +348 -64
  29. pgtask-0.4.0/crates/pgtask-postgres/tests/.gitignore +4 -0
  30. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-postgres/tests/corruption.rs +3 -1
  31. pgtask-0.4.0/crates/pgtask-postgres/tests/guarantees.rs +917 -0
  32. pgtask-0.4.0/crates/pgtask-postgres/tests/idle_delays.rs +169 -0
  33. pgtask-0.4.0/crates/pgtask-postgres/tests/linearizability.rs +1382 -0
  34. pgtask-0.4.0/crates/pgtask-postgres/tests/model.rs +476 -0
  35. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-postgres/tests/queue.rs +472 -9
  36. pgtask-0.4.0/crates/pgtask-postgres/tests/races.rs +364 -0
  37. pgtask-0.4.0/crates/pgtask-postgres/tests/result_wait_locking.rs +219 -0
  38. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-postgres/tests/results.rs +70 -0
  39. pgtask-0.4.0/crates/pgtask-postgres/tests/round_trip_properties.rs +200 -0
  40. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-postgres/tests/schedule.rs +140 -14
  41. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-postgres/tests/signals.rs +86 -0
  42. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-postgres/tests/sql_surface.rs +10 -0
  43. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-postgres/tests/sql_surface.txt +15 -0
  44. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-worker/Cargo.toml +3 -3
  45. pgtask-0.4.0/crates/pgtask-worker/src/lib.rs +18 -0
  46. pgtask-0.4.0/crates/pgtask-worker/src/outcome.rs +289 -0
  47. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-worker/src/registry.rs +35 -12
  48. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-worker/src/runtime.rs +644 -159
  49. pgtask-0.4.0/crates/pgtask-worker/tests/idle.rs +123 -0
  50. pgtask-0.4.0/crates/pgtask-worker/tests/lifecycle.rs +245 -0
  51. pgtask-0.4.0/crates/pgtask-worker/tests/outcomes.rs +298 -0
  52. pgtask-0.4.0/crates/pgtask-worker/tests/retention.rs +159 -0
  53. pgtask-0.4.0/crates/pgtask-worker/tests/schedule_failures.rs +168 -0
  54. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-worker/tests/worker.rs +522 -17
  55. {pgtask-0.2.0 → pgtask-0.4.0}/python/pgtask/__init__.py +5 -0
  56. {pgtask-0.2.0 → pgtask-0.4.0}/python/pgtask/_native.pyi +4 -0
  57. {pgtask-0.2.0 → pgtask-0.4.0}/python/pgtask/client.py +83 -5
  58. {pgtask-0.2.0 → pgtask-0.4.0}/sdks/python/Cargo.toml +1 -1
  59. {pgtask-0.2.0 → pgtask-0.4.0}/sdks/python/src/lib.rs +134 -8
  60. pgtask-0.4.0/sdks/python/tests/test_python_sdk.py +903 -0
  61. pgtask-0.2.0/crates/pgtask-worker/src/lib.rs +0 -11
  62. pgtask-0.2.0/sdks/python/tests/test_python_sdk.py +0 -447
  63. {pgtask-0.2.0 → pgtask-0.4.0}/README.md +0 -0
  64. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask/README.md +0 -0
  65. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask/src/lib.rs +0 -0
  66. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask/tests/public_api.rs +0 -0
  67. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-core/README.md +0 -0
  68. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-core/src/identifier.rs +0 -0
  69. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-core/src/retry.rs +0 -0
  70. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-core/tests/types.rs +0 -0
  71. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-otel/Cargo.toml +0 -0
  72. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-otel/README.md +0 -0
  73. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-otel/src/propagation.rs +0 -0
  74. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-otel/tests/propagation.rs +0 -0
  75. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-postgres/README.md +0 -0
  76. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-postgres/build.rs +0 -0
  77. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-postgres/migrations/0001_initial.sql +0 -0
  78. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-postgres/tests/sql_surface_baseline.txt +0 -0
  79. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-worker/README.md +0 -0
  80. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-worker/src/health.rs +0 -0
  81. {pgtask-0.2.0 → pgtask-0.4.0}/crates/pgtask-worker/tests/durable_restarts.rs +0 -0
  82. {pgtask-0.2.0 → pgtask-0.4.0}/pyproject.toml +0 -0
  83. {pgtask-0.2.0 → pgtask-0.4.0}/python/pgtask/py.typed +0 -0
  84. {pgtask-0.2.0 → pgtask-0.4.0}/sdks/python/README.md +0 -0
  85. {pgtask-0.2.0 → pgtask-0.4.0}/sdks/python/tests/__init__.py +0 -0
  86. {pgtask-0.2.0 → pgtask-0.4.0}/sdks/python/uv.lock +0 -0
@@ -155,6 +155,21 @@ version = "0.22.1"
155
155
  source = "registry+https://github.com/rust-lang/crates.io-index"
156
156
  checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
157
157
 
158
+ [[package]]
159
+ name = "bit-set"
160
+ version = "0.8.0"
161
+ source = "registry+https://github.com/rust-lang/crates.io-index"
162
+ checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
163
+ dependencies = [
164
+ "bit-vec",
165
+ ]
166
+
167
+ [[package]]
168
+ name = "bit-vec"
169
+ version = "0.8.0"
170
+ source = "registry+https://github.com/rust-lang/crates.io-index"
171
+ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
172
+
158
173
  [[package]]
159
174
  name = "bitflags"
160
175
  version = "2.13.1"
@@ -480,6 +495,12 @@ dependencies = [
480
495
  "pin-project-lite",
481
496
  ]
482
497
 
498
+ [[package]]
499
+ name = "fastrand"
500
+ version = "2.5.0"
501
+ source = "registry+https://github.com/rust-lang/crates.io-index"
502
+ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
503
+
483
504
  [[package]]
484
505
  name = "find-msvc-tools"
485
506
  version = "0.1.11"
@@ -497,6 +518,12 @@ dependencies = [
497
518
  "spin",
498
519
  ]
499
520
 
521
+ [[package]]
522
+ name = "fnv"
523
+ version = "1.0.7"
524
+ source = "registry+https://github.com/rust-lang/crates.io-index"
525
+ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
526
+
500
527
  [[package]]
501
528
  name = "foldhash"
502
529
  version = "0.2.0"
@@ -990,6 +1017,12 @@ dependencies = [
990
1017
  "vcpkg",
991
1018
  ]
992
1019
 
1020
+ [[package]]
1021
+ name = "linux-raw-sys"
1022
+ version = "0.12.1"
1023
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1024
+ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
1025
+
993
1026
  [[package]]
994
1027
  name = "litemap"
995
1028
  version = "0.8.3"
@@ -1162,7 +1195,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
1162
1195
 
1163
1196
  [[package]]
1164
1197
  name = "pgtask"
1165
- version = "0.2.0"
1198
+ version = "0.4.0"
1166
1199
  dependencies = [
1167
1200
  "pgtask-core",
1168
1201
  "pgtask-otel",
@@ -1175,7 +1208,7 @@ dependencies = [
1175
1208
 
1176
1209
  [[package]]
1177
1210
  name = "pgtask-bench"
1178
- version = "0.2.0"
1211
+ version = "0.4.0"
1179
1212
  dependencies = [
1180
1213
  "chrono",
1181
1214
  "pgtask",
@@ -1188,7 +1221,7 @@ dependencies = [
1188
1221
 
1189
1222
  [[package]]
1190
1223
  name = "pgtask-cli"
1191
- version = "0.2.0"
1224
+ version = "0.4.0"
1192
1225
  dependencies = [
1193
1226
  "clap",
1194
1227
  "pgtask",
@@ -1198,10 +1231,11 @@ dependencies = [
1198
1231
 
1199
1232
  [[package]]
1200
1233
  name = "pgtask-core"
1201
- version = "0.2.0"
1234
+ version = "0.4.0"
1202
1235
  dependencies = [
1203
1236
  "chrono",
1204
1237
  "cron",
1238
+ "proptest",
1205
1239
  "rand 0.9.5",
1206
1240
  "serde",
1207
1241
  "serde_json",
@@ -1211,7 +1245,7 @@ dependencies = [
1211
1245
 
1212
1246
  [[package]]
1213
1247
  name = "pgtask-otel"
1214
- version = "0.2.0"
1248
+ version = "0.4.0"
1215
1249
  dependencies = [
1216
1250
  "opentelemetry",
1217
1251
  "opentelemetry_sdk",
@@ -1222,11 +1256,13 @@ dependencies = [
1222
1256
 
1223
1257
  [[package]]
1224
1258
  name = "pgtask-postgres"
1225
- version = "0.2.0"
1259
+ version = "0.4.0"
1226
1260
  dependencies = [
1227
1261
  "chrono",
1228
1262
  "pgtask-core",
1229
1263
  "pgtask-otel",
1264
+ "proptest",
1265
+ "proptest-state-machine",
1230
1266
  "serde_json",
1231
1267
  "sqlx",
1232
1268
  "thiserror",
@@ -1237,7 +1273,7 @@ dependencies = [
1237
1273
 
1238
1274
  [[package]]
1239
1275
  name = "pgtask-python"
1240
- version = "0.2.0"
1276
+ version = "0.4.0"
1241
1277
  dependencies = [
1242
1278
  "chrono",
1243
1279
  "pgtask",
@@ -1254,7 +1290,7 @@ dependencies = [
1254
1290
 
1255
1291
  [[package]]
1256
1292
  name = "pgtask-web"
1257
- version = "0.2.0"
1293
+ version = "0.4.0"
1258
1294
  dependencies = [
1259
1295
  "axum",
1260
1296
  "chrono",
@@ -1273,7 +1309,7 @@ dependencies = [
1273
1309
 
1274
1310
  [[package]]
1275
1311
  name = "pgtask-worker"
1276
- version = "0.2.0"
1312
+ version = "0.4.0"
1277
1313
  dependencies = [
1278
1314
  "axum",
1279
1315
  "chrono",
@@ -1377,6 +1413,34 @@ dependencies = [
1377
1413
  "unicode-ident",
1378
1414
  ]
1379
1415
 
1416
+ [[package]]
1417
+ name = "proptest"
1418
+ version = "1.11.0"
1419
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1420
+ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
1421
+ dependencies = [
1422
+ "bit-set",
1423
+ "bit-vec",
1424
+ "bitflags",
1425
+ "num-traits",
1426
+ "rand 0.9.5",
1427
+ "rand_chacha",
1428
+ "rand_xorshift",
1429
+ "regex-syntax",
1430
+ "rusty-fork",
1431
+ "tempfile",
1432
+ "unarray",
1433
+ ]
1434
+
1435
+ [[package]]
1436
+ name = "proptest-state-machine"
1437
+ version = "0.3.1"
1438
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1439
+ checksum = "e943d140e09d07740fb496487c51fb8eb31c70389ac4a2e9dcd8a0d9fdf228d4"
1440
+ dependencies = [
1441
+ "proptest",
1442
+ ]
1443
+
1380
1444
  [[package]]
1381
1445
  name = "pyo3"
1382
1446
  version = "0.29.2"
@@ -1458,6 +1522,12 @@ dependencies = [
1458
1522
  "serde",
1459
1523
  ]
1460
1524
 
1525
+ [[package]]
1526
+ name = "quick-error"
1527
+ version = "1.2.3"
1528
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1529
+ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
1530
+
1461
1531
  [[package]]
1462
1532
  name = "quote"
1463
1533
  version = "1.0.47"
@@ -1540,6 +1610,15 @@ version = "0.10.1"
1540
1610
  source = "registry+https://github.com/rust-lang/crates.io-index"
1541
1611
  checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
1542
1612
 
1613
+ [[package]]
1614
+ name = "rand_xorshift"
1615
+ version = "0.4.0"
1616
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1617
+ checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
1618
+ dependencies = [
1619
+ "rand_core 0.9.5",
1620
+ ]
1621
+
1543
1622
  [[package]]
1544
1623
  name = "redox_syscall"
1545
1624
  version = "0.5.18"
@@ -1580,11 +1659,24 @@ dependencies = [
1580
1659
  "windows-sys 0.52.0",
1581
1660
  ]
1582
1661
 
1662
+ [[package]]
1663
+ name = "rustix"
1664
+ version = "1.1.4"
1665
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1666
+ checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
1667
+ dependencies = [
1668
+ "bitflags",
1669
+ "errno",
1670
+ "libc",
1671
+ "linux-raw-sys",
1672
+ "windows-sys 0.61.2",
1673
+ ]
1674
+
1583
1675
  [[package]]
1584
1676
  name = "rustls"
1585
- version = "0.23.43"
1677
+ version = "0.23.45"
1586
1678
  source = "registry+https://github.com/rust-lang/crates.io-index"
1587
- checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
1679
+ checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
1588
1680
  dependencies = [
1589
1681
  "once_cell",
1590
1682
  "ring",
@@ -1632,6 +1724,18 @@ version = "1.0.23"
1632
1724
  source = "registry+https://github.com/rust-lang/crates.io-index"
1633
1725
  checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
1634
1726
 
1727
+ [[package]]
1728
+ name = "rusty-fork"
1729
+ version = "0.3.1"
1730
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1731
+ checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
1732
+ dependencies = [
1733
+ "fnv",
1734
+ "quick-error",
1735
+ "tempfile",
1736
+ "wait-timeout",
1737
+ ]
1738
+
1635
1739
  [[package]]
1636
1740
  name = "ryu"
1637
1741
  version = "1.0.23"
@@ -2095,6 +2199,19 @@ version = "0.13.5"
2095
2199
  source = "registry+https://github.com/rust-lang/crates.io-index"
2096
2200
  checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
2097
2201
 
2202
+ [[package]]
2203
+ name = "tempfile"
2204
+ version = "3.27.0"
2205
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2206
+ checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
2207
+ dependencies = [
2208
+ "fastrand",
2209
+ "getrandom 0.4.3",
2210
+ "once_cell",
2211
+ "rustix",
2212
+ "windows-sys 0.61.2",
2213
+ ]
2214
+
2098
2215
  [[package]]
2099
2216
  name = "thiserror"
2100
2217
  version = "2.0.20"
@@ -2312,6 +2429,12 @@ version = "1.20.1"
2312
2429
  source = "registry+https://github.com/rust-lang/crates.io-index"
2313
2430
  checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
2314
2431
 
2432
+ [[package]]
2433
+ name = "unarray"
2434
+ version = "0.1.4"
2435
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2436
+ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
2437
+
2315
2438
  [[package]]
2316
2439
  name = "unicode-bidi"
2317
2440
  version = "0.3.18"
@@ -2399,6 +2522,15 @@ version = "0.9.5"
2399
2522
  source = "registry+https://github.com/rust-lang/crates.io-index"
2400
2523
  checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
2401
2524
 
2525
+ [[package]]
2526
+ name = "wait-timeout"
2527
+ version = "0.2.1"
2528
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2529
+ checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
2530
+ dependencies = [
2531
+ "libc",
2532
+ ]
2533
+
2402
2534
  [[package]]
2403
2535
  name = "wasi"
2404
2536
  version = "0.11.1+wasi-snapshot-preview1"
@@ -7,7 +7,7 @@ edition = "2024"
7
7
  license = "MIT"
8
8
  repository = "https://github.com/Kludex/pgtask"
9
9
  rust-version = "1.94"
10
- version = "0.2.0"
10
+ version = "0.4.0"
11
11
 
12
12
  [workspace.dependencies]
13
13
  axum = "0.8.9"
@@ -18,6 +18,8 @@ futures = "0.3.34"
18
18
  html-escape = "0.2.15"
19
19
  opentelemetry = "0.32.0"
20
20
  opentelemetry_sdk = "0.32.1"
21
+ proptest = "1"
22
+ proptest-state-machine = "0.3"
21
23
  pyo3 = "0.29.2"
22
24
  pyo3-async-runtimes = "0.29.0"
23
25
  pythonize = "0.29.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgtask
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Classifier: Development Status :: 2 - Pre-Alpha
5
5
  Classifier: Programming Language :: Python :: 3 :: Only
6
6
  Classifier: Programming Language :: Python :: 3.10
@@ -9,10 +9,10 @@ version.workspace = true
9
9
  readme = "README.md"
10
10
 
11
11
  [dependencies]
12
- pgtask-core = { path = "../pgtask-core", version = "0.2.0" }
13
- pgtask-otel = { path = "../pgtask-otel", version = "0.2.0" }
14
- pgtask-postgres = { path = "../pgtask-postgres", version = "0.2.0" }
15
- pgtask-worker = { path = "../pgtask-worker", version = "0.2.0" }
12
+ pgtask-core = { path = "../pgtask-core", version = "0.4.0" }
13
+ pgtask-otel = { path = "../pgtask-otel", version = "0.4.0" }
14
+ pgtask-postgres = { path = "../pgtask-postgres", version = "0.4.0" }
15
+ pgtask-worker = { path = "../pgtask-worker", version = "0.4.0" }
16
16
 
17
17
  [dev-dependencies]
18
18
  serde_json.workspace = true
@@ -17,5 +17,8 @@ serde_json.workspace = true
17
17
  thiserror.workspace = true
18
18
  uuid.workspace = true
19
19
 
20
+ [dev-dependencies]
21
+ proptest.workspace = true
22
+
20
23
  [lints]
21
24
  workspace = true
@@ -20,7 +20,7 @@ pub use task::{
20
20
  pub const STORAGE_PROTOCOL_MIN_VERSION: u32 = 1;
21
21
 
22
22
  /// Newest storage protocol understood by this release.
23
- pub const STORAGE_PROTOCOL_MAX_VERSION: u32 = 1;
23
+ pub const STORAGE_PROTOCOL_MAX_VERSION: u32 = 2;
24
24
 
25
25
  /// Current storage protocol emitted by this release.
26
26
  pub const STORAGE_PROTOCOL_VERSION: u32 = STORAGE_PROTOCOL_MAX_VERSION;
@@ -14,9 +14,7 @@ pub enum ScheduleDefinition {
14
14
 
15
15
  impl ScheduleDefinition {
16
16
  pub fn interval(every: Duration) -> Result<Self, ScheduleError> {
17
- if every.is_zero() {
18
- return Err(ScheduleError::ZeroInterval);
19
- }
17
+ interval_milliseconds(every)?;
20
18
  TimeDelta::from_std(every).map_err(|_| ScheduleError::IntervalOutOfRange)?;
21
19
  Ok(Self::Interval { every })
22
20
  }
@@ -47,11 +45,7 @@ impl ScheduleDefinition {
47
45
  fn due_count(&self, first_due: DateTime<Utc>, now: DateTime<Utc>) -> Result<u64, ScheduleError> {
48
46
  match self {
49
47
  Self::Interval { every } => {
50
- let every_milliseconds =
51
- i64::try_from(every.as_millis()).map_err(|_| ScheduleError::IntervalOutOfRange)?;
52
- if every_milliseconds == 0 {
53
- return Err(ScheduleError::ZeroInterval);
54
- }
48
+ let every_milliseconds = interval_milliseconds(*every)?;
55
49
  let elapsed_milliseconds = (now - first_due).num_milliseconds().max(0);
56
50
  Ok(u64::try_from(elapsed_milliseconds / every_milliseconds).unwrap_or(0) + 1)
57
51
  }
@@ -70,8 +64,7 @@ impl ScheduleDefinition {
70
64
  fn latest_due(&self, first_due: DateTime<Utc>, now: DateTime<Utc>) -> Result<DateTime<Utc>, ScheduleError> {
71
65
  match self {
72
66
  Self::Interval { every } => {
73
- let every_milliseconds =
74
- i64::try_from(every.as_millis()).map_err(|_| ScheduleError::IntervalOutOfRange)?;
67
+ let every_milliseconds = interval_milliseconds(*every)?;
75
68
  let elapsed_milliseconds = (now - first_due).num_milliseconds();
76
69
  let intervals = elapsed_milliseconds / every_milliseconds;
77
70
  first_due
@@ -198,6 +191,8 @@ pub struct Materialization {
198
191
  pub enum ScheduleError {
199
192
  #[error("interval must be greater than zero")]
200
193
  ZeroInterval,
194
+ #[error("interval must use whole-millisecond precision")]
195
+ UnsupportedIntervalPrecision,
201
196
  #[error("interval exceeds the supported date range")]
202
197
  IntervalOutOfRange,
203
198
  #[error("cron expression must contain exactly six fields: second minute hour day-of-month month day-of-week")]
@@ -210,6 +205,16 @@ pub enum ScheduleError {
210
205
  DateOutOfRange,
211
206
  }
212
207
 
208
+ fn interval_milliseconds(every: Duration) -> Result<i64, ScheduleError> {
209
+ if every.is_zero() {
210
+ return Err(ScheduleError::ZeroInterval);
211
+ }
212
+ if !every.as_nanos().is_multiple_of(1_000_000) {
213
+ return Err(ScheduleError::UnsupportedIntervalPrecision);
214
+ }
215
+ i64::try_from(every.as_millis()).map_err(|_| ScheduleError::IntervalOutOfRange)
216
+ }
217
+
213
218
  fn parse_cron(expression: &str) -> Result<CronSchedule, ScheduleError> {
214
219
  if expression.split_whitespace().count() != 6 {
215
220
  return Err(ScheduleError::InvalidCronFieldCount);
@@ -159,6 +159,7 @@ pub struct Task {
159
159
  pub priority: i16,
160
160
  pub run_at: DateTime<Utc>,
161
161
  pub attempt: u16,
162
+ pub failed_attempts: u16,
162
163
  pub max_attempts: u16,
163
164
  pub retry_policy: Option<RetryPolicy>,
164
165
  pub lease_token: Option<LeaseToken>,
@@ -0,0 +1,4 @@
1
+ # proptest writes failing seeds here. They are worth committing when they
2
+ # still reproduce, but a seed recorded under a generator that has since changed
3
+ # replays as a different value and only misleads.
4
+ *.proptest-regressions
@@ -0,0 +1,124 @@
1
+ //! Property tests for retry backoff.
2
+ //!
3
+ //! `delay_for` decides how long a failed task waits before it is claimed again.
4
+ //! It is pure apart from the jitter, and the jitter is exactly what makes
5
+ //! example-based tests weak here: a single call proves nothing about the bound.
6
+ //!
7
+ //! Case count follows `PROPTEST_CASES`.
8
+
9
+ use std::time::Duration;
10
+
11
+ use pgtask_core::RetryPolicy;
12
+ use proptest::prelude::*;
13
+
14
+ fn cases() -> u32 {
15
+ std::env::var("PROPTEST_CASES")
16
+ .ok()
17
+ .and_then(|value| value.parse().ok())
18
+ .unwrap_or(512)
19
+ }
20
+
21
+ fn config() -> ProptestConfig {
22
+ ProptestConfig {
23
+ cases: cases(),
24
+ ..ProptestConfig::default()
25
+ }
26
+ }
27
+
28
+ /// Exponential policies the database would accept: a positive factor and a
29
+ /// ceiling at least as large as the base, matching the CHECK constraint on
30
+ /// `pgtask.handler_policies`.
31
+ fn storable_exponential() -> impl Strategy<Value = RetryPolicy> {
32
+ (0_u64..3_600_000, 1_u32..16, 0_u64..86_400_000).prop_map(|(base, factor, extra)| RetryPolicy::Exponential {
33
+ base_delay: Duration::from_millis(base),
34
+ factor,
35
+ max_delay: Duration::from_millis(base + extra),
36
+ })
37
+ }
38
+
39
+ proptest! {
40
+ #![proptest_config(config())]
41
+
42
+ /// The ceiling is the promise the policy makes. Jitter may shorten a delay,
43
+ /// never lengthen it past `max_delay`.
44
+ #[test]
45
+ fn an_exponential_delay_never_exceeds_its_ceiling(
46
+ policy in storable_exponential(),
47
+ attempt in 0_u16..2_000,
48
+ ) {
49
+ let RetryPolicy::Exponential { max_delay, .. } = policy else {
50
+ unreachable!("the strategy only builds exponential policies")
51
+ };
52
+ let delay = policy.delay_for(attempt).expect("exponential always yields a delay");
53
+ prop_assert!(
54
+ delay <= max_delay,
55
+ "attempt {attempt} waited {delay:?}, past the {max_delay:?} ceiling"
56
+ );
57
+ }
58
+
59
+ /// Growth is bounded by the policy's own curve, not just by the ceiling, so
60
+ /// an early attempt cannot wait as long as a late one.
61
+ #[test]
62
+ fn an_exponential_delay_stays_under_the_curve(
63
+ base_millis in 1_u64..10_000,
64
+ factor in 2_u32..8,
65
+ attempt in 1_u16..8,
66
+ ) {
67
+ let policy = RetryPolicy::Exponential {
68
+ base_delay: Duration::from_millis(base_millis),
69
+ factor,
70
+ // No effective ceiling, so this measures the curve rather than the clamp.
71
+ max_delay: Duration::MAX,
72
+ };
73
+ // Derived here rather than read from the implementation.
74
+ let expected_ceiling = Duration::from_millis(
75
+ base_millis * u64::from(factor).pow(u32::from(attempt) - 1),
76
+ );
77
+ let delay = policy.delay_for(attempt).unwrap();
78
+ prop_assert!(
79
+ delay <= expected_ceiling,
80
+ "attempt {attempt} waited {delay:?}, past the {expected_ceiling:?} the curve allows"
81
+ );
82
+ }
83
+
84
+ /// A fixed policy is exactly that: the same wait every time, with no jitter.
85
+ #[test]
86
+ fn a_fixed_delay_is_constant(millis in 0_u64..86_400_000, attempt in 0_u16..2_000) {
87
+ let delay = Duration::from_millis(millis);
88
+ prop_assert_eq!(RetryPolicy::Fixed { delay }.delay_for(attempt), Some(delay));
89
+ }
90
+
91
+ /// `Never` means never, at any attempt.
92
+ #[test]
93
+ fn never_never_retries(attempt in 0_u16..u16::MAX) {
94
+ prop_assert_eq!(RetryPolicy::Never.delay_for(attempt), None);
95
+ }
96
+
97
+ /// Attempt numbers come from the database, so the whole `u16` range has to
98
+ /// be survivable — including the saturating edges.
99
+ #[test]
100
+ fn delays_are_computable_for_any_attempt(
101
+ policy in storable_exponential(),
102
+ attempt in any::<u16>(),
103
+ ) {
104
+ let _ = policy.delay_for(attempt);
105
+ }
106
+ }
107
+
108
+ /// The two edges the range strategies above will essentially never generate.
109
+ #[test]
110
+ fn the_extreme_attempt_numbers_are_survivable() {
111
+ let policy = RetryPolicy::default();
112
+ assert!(policy.delay_for(0).is_some(), "attempt 0 must not panic");
113
+ assert!(policy.delay_for(u16::MAX).is_some(), "the last attempt must not panic");
114
+
115
+ let saturating = RetryPolicy::Exponential {
116
+ base_delay: Duration::from_secs(u64::MAX / 2),
117
+ factor: u32::MAX,
118
+ max_delay: Duration::MAX,
119
+ };
120
+ assert!(
121
+ saturating.delay_for(u16::MAX).is_some(),
122
+ "a policy that overflows every multiplication must saturate, not panic"
123
+ );
124
+ }
@@ -115,13 +115,14 @@ fn a_cron_schedule_reports_discarded_occurrences() {
115
115
  }
116
116
 
117
117
  #[test]
118
- fn a_sub_millisecond_interval_is_rejected_rather_than_dividing_by_zero() {
119
- // `interval` only rejects a zero duration, so an interval below a millisecond reaches
120
- // materialization and truncates to zero milliseconds.
121
- let schedule = ScheduleDefinition::interval(Duration::from_micros(500)).unwrap();
122
- let first_due = Utc.with_ymd_and_hms(2026, 8, 15, 12, 0, 0).unwrap();
123
- let now = first_due + chrono::TimeDelta::seconds(1);
124
-
125
- let error = schedule.materialize(first_due, now, MisfirePolicy::Skip).unwrap_err();
126
- assert!(matches!(error, ScheduleError::ZeroInterval));
118
+ fn a_fractional_millisecond_interval_is_rejected() {
119
+ for interval in [Duration::from_micros(500), Duration::from_micros(1_500)] {
120
+ let error = ScheduleDefinition::interval(interval).unwrap_err();
121
+ assert!(matches!(error, ScheduleError::UnsupportedIntervalPrecision));
122
+
123
+ let schedule = ScheduleDefinition::Interval { every: interval };
124
+ let now = Utc.with_ymd_and_hms(2026, 8, 15, 12, 0, 0).unwrap();
125
+ let error = schedule.materialize(now, now, MisfirePolicy::Latest).unwrap_err();
126
+ assert!(matches!(error, ScheduleError::UnsupportedIntervalPrecision));
127
+ }
127
128
  }