rubydb 0.1.4 → 0.1.5

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 (420) hide show
  1. checksums.yaml +4 -4
  2. data/.github/PULL_REQUEST_TEMPLATE.md +15 -15
  3. data/.github/workflows/benchmark.yml +26 -26
  4. data/.github/workflows/compatibility.yml +63 -63
  5. data/.github/workflows/fuzz.yml +33 -33
  6. data/.github/workflows/lint.yml +21 -21
  7. data/.github/workflows/operations.yml +24 -24
  8. data/.github/workflows/production-validation.yml +111 -111
  9. data/.github/workflows/release.yml +77 -77
  10. data/.github/workflows/security.yml +39 -37
  11. data/.github/workflows/test.yml +26 -26
  12. data/.github/workflows/workload.yml +58 -58
  13. data/.gitignore +12 -5
  14. data/.rubocop.yml +50 -44
  15. data/.standard.yml +9 -14
  16. data/ARCHITECTURE.md +21 -21
  17. data/CHANGELOG.md +43 -27
  18. data/CODE_OF_CONDUCT.md +13 -13
  19. data/CONTRIBUTING.md +29 -29
  20. data/GOVERNANCE.md +16 -16
  21. data/Gemfile +18 -17
  22. data/Gemfile.lock +125 -71
  23. data/README.md +249 -162
  24. data/ROADMAP.md +27 -27
  25. data/Rakefile +71 -71
  26. data/SECURITY.md +54 -54
  27. data/SUPPORT.md +14 -14
  28. data/adapters/activerecord/Gemfile +11 -11
  29. data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +873 -879
  30. data/adapters/activerecord/rubydb-activerecord.gemspec +20 -20
  31. data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +143 -143
  32. data/adapters/ruby/README.md +18 -18
  33. data/adapters/sequel/README.md +11 -11
  34. data/config/monitoring/prometheus-alerts.yml +39 -39
  35. data/config/production.yml +36 -36
  36. data/docs/README.md +75 -71
  37. data/docs/architecture/concurrency.md +14 -14
  38. data/docs/architecture/current-state.md +125 -125
  39. data/docs/architecture/execution-engine.md +25 -25
  40. data/docs/architecture/indexes.md +19 -19
  41. data/docs/architecture/mvcc.md +19 -19
  42. data/docs/architecture/overview.md +13 -13
  43. data/docs/architecture/pages.md +11 -11
  44. data/docs/architecture/production-roadmap.md +82 -82
  45. data/docs/architecture/query-planner.md +20 -20
  46. data/docs/architecture/recovery.md +18 -18
  47. data/docs/architecture/sql-engine.md +12 -12
  48. data/docs/architecture/storage-engine.md +14 -14
  49. data/docs/architecture/transactions.md +10 -10
  50. data/docs/architecture/wal.md +28 -28
  51. data/docs/cli-cheatsheet.md +98 -98
  52. data/docs/cli.md +275 -275
  53. data/docs/contributing/architecture.md +9 -9
  54. data/docs/contributing/benchmarking.md +14 -14
  55. data/docs/contributing/development.md +16 -16
  56. data/docs/contributing/release-process.md +49 -49
  57. data/docs/contributing/testing.md +16 -16
  58. data/docs/debugging.md +229 -229
  59. data/docs/developer/branching.md +10 -10
  60. data/docs/developer/database-diff.md +10 -10
  61. data/docs/developer/local-development.md +17 -17
  62. data/docs/developer/snapshots.md +9 -9
  63. data/docs/developer/temporal-data.md +10 -10
  64. data/docs/developer-guide.md +297 -297
  65. data/docs/getting-started/first-database.md +16 -16
  66. data/docs/getting-started/first-query.md +13 -13
  67. data/docs/getting-started/installation.md +19 -19
  68. data/docs/getting-started/local-to-production.md +300 -300
  69. data/docs/getting-started/quickstart.md +17 -17
  70. data/docs/getting-started/rails.md +16 -16
  71. data/docs/hardening_backlog.md +93 -93
  72. data/docs/lessons-learned.md +112 -112
  73. data/docs/operations/backups.md +33 -33
  74. data/docs/operations/disaster-recovery.md +31 -31
  75. data/docs/operations/failover.md +30 -30
  76. data/docs/operations/monitoring.md +25 -25
  77. data/docs/operations/production-guide.md +295 -295
  78. data/docs/operations/production-runbook.md +45 -45
  79. data/docs/operations/replication.md +33 -33
  80. data/docs/operations/restore.md +6 -6
  81. data/docs/operations/runbook.md +34 -34
  82. data/docs/operations/upgrades.md +14 -14
  83. data/docs/operations/workload-testing.md +17 -17
  84. data/docs/production-readiness.md +118 -118
  85. data/docs/production_validation.md +150 -150
  86. data/docs/rails/active-record.md +11 -11
  87. data/docs/rails/compatibility-guide.md +90 -90
  88. data/docs/rails/database-yml.md +92 -92
  89. data/docs/rails/installation.md +17 -17
  90. data/docs/rails/migrations.md +17 -17
  91. data/docs/rails/production.md +82 -82
  92. data/docs/rails/troubleshooting.md +18 -18
  93. data/docs/release.md +25 -25
  94. data/docs/server/architecture.md +10 -10
  95. data/docs/server/authentication.md +10 -10
  96. data/docs/server/configuration.md +16 -16
  97. data/docs/server/connection-pooling.md +10 -10
  98. data/docs/server/deployment.md +10 -10
  99. data/docs/server/protocol.md +12 -12
  100. data/docs/sql/compatibility-guide.md +82 -82
  101. data/docs/sql/compatibility.md +39 -39
  102. data/docs/sql/data-types.md +10 -10
  103. data/docs/sql/functions.md +9 -9
  104. data/docs/sql/joins.md +9 -9
  105. data/docs/sql/operators.md +9 -9
  106. data/docs/sql/sqlite-compatibility.md +21 -21
  107. data/docs/sql/syntax.md +10 -10
  108. data/docs/sql/transactions.md +10 -10
  109. data/docs/troubleshooting.md +244 -244
  110. data/lessons/01-foundations.md +73 -0
  111. data/lessons/02-local-development.md +121 -0
  112. data/lessons/03-embedded-rubydb.md +99 -0
  113. data/lessons/04-rails-complex-apps.md +138 -0
  114. data/lessons/05-rubydb-production-server.md +237 -0
  115. data/lessons/06-postgresql-massive-apps.md +96 -0
  116. data/lessons/07-hybrid-microservices.md +94 -0
  117. data/lessons/08-migrations-backups-recovery.md +86 -0
  118. data/lessons/09-observability-security-scale.md +87 -0
  119. data/lessons/10-release-readiness.md +125 -0
  120. data/lib/rubydb/backup/archive.rb +332 -334
  121. data/lib/rubydb/backup/backup.rb +400 -401
  122. data/lib/rubydb/backup/incremental.rb +349 -353
  123. data/lib/rubydb/backup/restore.rb +289 -290
  124. data/lib/rubydb/backup/snapshot.rb +265 -267
  125. data/lib/rubydb/backup/verification.rb +276 -279
  126. data/lib/rubydb/branching/branch.rb +181 -181
  127. data/lib/rubydb/branching/branch_manager.rb +307 -311
  128. data/lib/rubydb/branching/branch_metadata.rb +140 -140
  129. data/lib/rubydb/branching/checkout.rb +165 -166
  130. data/lib/rubydb/branching/copy_on_write.rb +272 -272
  131. data/lib/rubydb/branching/diff.rb +137 -138
  132. data/lib/rubydb/branching/merge.rb +282 -285
  133. data/lib/rubydb/build_info.rb +15 -15
  134. data/lib/rubydb/catalog/catalog.rb +391 -391
  135. data/lib/rubydb/catalog/column.rb +112 -112
  136. data/lib/rubydb/catalog/constraint.rb +180 -180
  137. data/lib/rubydb/catalog/database.rb +184 -184
  138. data/lib/rubydb/catalog/index.rb +97 -97
  139. data/lib/rubydb/catalog/schema.rb +103 -103
  140. data/lib/rubydb/catalog/sequence.rb +90 -90
  141. data/lib/rubydb/catalog/system_catalog.rb +698 -698
  142. data/lib/rubydb/catalog/table.rb +178 -178
  143. data/lib/rubydb/catalog/trigger.rb +102 -102
  144. data/lib/rubydb/catalog/view.rb +66 -66
  145. data/lib/rubydb/cli/application.rb +163 -163
  146. data/lib/rubydb/cli/commands/backup.rb +80 -81
  147. data/lib/rubydb/cli/commands/branch.rb +72 -72
  148. data/lib/rubydb/cli/commands/checkout.rb +54 -54
  149. data/lib/rubydb/cli/commands/create.rb +58 -58
  150. data/lib/rubydb/cli/commands/diff.rb +76 -77
  151. data/lib/rubydb/cli/commands/doctor.rb +74 -74
  152. data/lib/rubydb/cli/commands/drop.rb +57 -57
  153. data/lib/rubydb/cli/commands/init.rb +101 -102
  154. data/lib/rubydb/cli/commands/inspect.rb +95 -95
  155. data/lib/rubydb/cli/commands/merge.rb +63 -63
  156. data/lib/rubydb/cli/commands/migrate.rb +62 -62
  157. data/lib/rubydb/cli/commands/restart.rb +42 -39
  158. data/lib/rubydb/cli/commands/restore.rb +121 -121
  159. data/lib/rubydb/cli/commands/shell.rb +365 -365
  160. data/lib/rubydb/cli/commands/snapshot.rb +79 -79
  161. data/lib/rubydb/cli/commands/start.rb +88 -82
  162. data/lib/rubydb/cli/commands/status.rb +96 -92
  163. data/lib/rubydb/cli/commands/stop.rb +47 -47
  164. data/lib/rubydb/cli/commands/vacuum.rb +58 -58
  165. data/lib/rubydb/cli/formatter.rb +221 -221
  166. data/lib/rubydb/cli/output.rb +168 -168
  167. data/lib/rubydb/client/client.rb +302 -304
  168. data/lib/rubydb/client/connection.rb +414 -415
  169. data/lib/rubydb/client/connection_pool.rb +168 -168
  170. data/lib/rubydb/client/connection_url.rb +96 -96
  171. data/lib/rubydb/client/prepared_statement.rb +60 -60
  172. data/lib/rubydb/client/result.rb +123 -123
  173. data/lib/rubydb/client/statement.rb +52 -52
  174. data/lib/rubydb/client/transaction.rb +130 -130
  175. data/lib/rubydb/concurrency/concurrency.rb +19 -19
  176. data/lib/rubydb/concurrency/deadlock_detector.rb +148 -150
  177. data/lib/rubydb/concurrency/latch.rb +101 -101
  178. data/lib/rubydb/concurrency/lock_graph.rb +163 -165
  179. data/lib/rubydb/concurrency/mutex.rb +181 -183
  180. data/lib/rubydb/concurrency/rw_lock.rb +180 -180
  181. data/lib/rubydb/concurrency/scheduler.rb +248 -250
  182. data/lib/rubydb/concurrency/worker_pool.rb +145 -143
  183. data/lib/rubydb/configuration/config.rb +170 -170
  184. data/lib/rubydb/configuration/defaults.rb +179 -179
  185. data/lib/rubydb/configuration/environment.rb +152 -152
  186. data/lib/rubydb/configuration/parser.rb +185 -185
  187. data/lib/rubydb/configuration/validation.rb +221 -221
  188. data/lib/rubydb/constants.rb +74 -74
  189. data/lib/rubydb/constraints/check.rb +181 -181
  190. data/lib/rubydb/constraints/constraint.rb +101 -101
  191. data/lib/rubydb/constraints/foreign_key.rb +130 -130
  192. data/lib/rubydb/constraints/not_null.rb +64 -64
  193. data/lib/rubydb/constraints/primary_key.rb +99 -99
  194. data/lib/rubydb/constraints/unique.rb +106 -108
  195. data/lib/rubydb/constraints/validator.rb +349 -350
  196. data/lib/rubydb/errors/authentication_error.rb +10 -10
  197. data/lib/rubydb/errors/authorization_error.rb +23 -23
  198. data/lib/rubydb/errors/client_error.rb +10 -10
  199. data/lib/rubydb/errors/configuration_error.rb +10 -10
  200. data/lib/rubydb/errors/connection_error.rb +10 -10
  201. data/lib/rubydb/errors/constraint_error.rb +23 -23
  202. data/lib/rubydb/errors/corruption_error.rb +10 -10
  203. data/lib/rubydb/errors/database_error.rb +10 -10
  204. data/lib/rubydb/errors/error.rb +20 -20
  205. data/lib/rubydb/errors/execution_error.rb +10 -10
  206. data/lib/rubydb/errors/parser_error.rb +10 -10
  207. data/lib/rubydb/errors/recovery_error.rb +10 -10
  208. data/lib/rubydb/errors/replication_error.rb +10 -10
  209. data/lib/rubydb/errors/server_error.rb +6 -6
  210. data/lib/rubydb/errors/storage_error.rb +10 -10
  211. data/lib/rubydb/errors/transaction_error.rb +10 -10
  212. data/lib/rubydb/execution/aggregate_executor.rb +134 -138
  213. data/lib/rubydb/execution/delete_executor.rb +110 -112
  214. data/lib/rubydb/execution/distinct_executor.rb +131 -135
  215. data/lib/rubydb/execution/executor.rb +1182 -1188
  216. data/lib/rubydb/execution/expression.rb +191 -193
  217. data/lib/rubydb/execution/index_scan.rb +142 -142
  218. data/lib/rubydb/execution/insert_executor.rb +215 -217
  219. data/lib/rubydb/execution/join_executor.rb +243 -249
  220. data/lib/rubydb/execution/limit_executor.rb +83 -85
  221. data/lib/rubydb/execution/optimizer.rb +227 -215
  222. data/lib/rubydb/execution/plan.rb +355 -353
  223. data/lib/rubydb/execution/planner.rb +542 -536
  224. data/lib/rubydb/execution/predicate.rb +235 -235
  225. data/lib/rubydb/execution/scan.rb +49 -49
  226. data/lib/rubydb/execution/sequential_scan.rb +63 -63
  227. data/lib/rubydb/execution/sort_executor.rb +180 -185
  228. data/lib/rubydb/execution/update_executor.rb +160 -162
  229. data/lib/rubydb/functions/aggregate.rb +70 -70
  230. data/lib/rubydb/functions/date_functions.rb +274 -278
  231. data/lib/rubydb/functions/function.rb +85 -85
  232. data/lib/rubydb/functions/json_functions.rb +231 -215
  233. data/lib/rubydb/functions/numeric_functions.rb +346 -346
  234. data/lib/rubydb/functions/scalar.rb +52 -52
  235. data/lib/rubydb/functions/string_functions.rb +383 -383
  236. data/lib/rubydb/functions/system_functions.rb +258 -246
  237. data/lib/rubydb/history/as_of.rb +238 -238
  238. data/lib/rubydb/history/change.rb +105 -105
  239. data/lib/rubydb/history/history.rb +131 -131
  240. data/lib/rubydb/history/history_manager.rb +228 -229
  241. data/lib/rubydb/history/temporal_query.rb +202 -202
  242. data/lib/rubydb/history/timeline.rb +144 -144
  243. data/lib/rubydb/indexes/btree.rb +186 -186
  244. data/lib/rubydb/indexes/btree_cursor.rb +258 -258
  245. data/lib/rubydb/indexes/btree_node.rb +384 -385
  246. data/lib/rubydb/indexes/hash_index.rb +150 -150
  247. data/lib/rubydb/indexes/index.rb +71 -71
  248. data/lib/rubydb/indexes/index_manager.rb +408 -406
  249. data/lib/rubydb/indexes/index_scan.rb +466 -470
  250. data/lib/rubydb/migrations/migration.rb +253 -254
  251. data/lib/rubydb/migrations/migration_lock.rb +146 -146
  252. data/lib/rubydb/migrations/migration_manager.rb +187 -176
  253. data/lib/rubydb/migrations/migration_version.rb +71 -71
  254. data/lib/rubydb/migrations/schema_diff.rb +211 -211
  255. data/lib/rubydb/migrations/schema_version.rb +64 -64
  256. data/lib/rubydb/monitoring/events.rb +155 -160
  257. data/lib/rubydb/monitoring/health.rb +216 -222
  258. data/lib/rubydb/monitoring/logger.rb +188 -193
  259. data/lib/rubydb/monitoring/metrics.rb +363 -359
  260. data/lib/rubydb/monitoring/performance.rb +176 -176
  261. data/lib/rubydb/monitoring/statistics.rb +168 -170
  262. data/lib/rubydb/mvcc/garbage_collector.rb +199 -199
  263. data/lib/rubydb/mvcc/mvcc.rb +16 -16
  264. data/lib/rubydb/mvcc/snapshot.rb +146 -147
  265. data/lib/rubydb/mvcc/vacuum.rb +180 -180
  266. data/lib/rubydb/mvcc/version.rb +106 -106
  267. data/lib/rubydb/mvcc/version_store.rb +396 -398
  268. data/lib/rubydb/mvcc/visibility.rb +107 -109
  269. data/lib/rubydb/protocol/capabilities.rb +125 -125
  270. data/lib/rubydb/protocol/decoder.rb +142 -145
  271. data/lib/rubydb/protocol/encoder.rb +131 -136
  272. data/lib/rubydb/protocol/handshake.rb +306 -305
  273. data/lib/rubydb/protocol/message.rb +121 -121
  274. data/lib/rubydb/protocol/parameter_binder.rb +101 -0
  275. data/lib/rubydb/protocol/protocol.rb +276 -277
  276. data/lib/rubydb/protocol/version.rb +54 -54
  277. data/lib/rubydb/rails/adapter.rb +245 -239
  278. data/lib/rubydb/rails/connection.rb +312 -314
  279. data/lib/rubydb/rails/database_statements.rb +122 -122
  280. data/lib/rubydb/rails/migration.rb +131 -131
  281. data/lib/rubydb/rails/quoting.rb +109 -109
  282. data/lib/rubydb/rails/result.rb +117 -117
  283. data/lib/rubydb/rails/schema_statements.rb +339 -339
  284. data/lib/rubydb/rails/transaction.rb +105 -105
  285. data/lib/rubydb/rails/type.rb +126 -126
  286. data/lib/rubydb/recovery/checkpoint.rb +261 -257
  287. data/lib/rubydb/recovery/consistency.rb +457 -467
  288. data/lib/rubydb/recovery/corruption_detector.rb +5 -5
  289. data/lib/rubydb/recovery/crash_recovery.rb +381 -387
  290. data/lib/rubydb/recovery/recovery_manager.rb +204 -206
  291. data/lib/rubydb/recovery/redo.rb +235 -237
  292. data/lib/rubydb/recovery/undo.rb +204 -206
  293. data/lib/rubydb/replication/failover.rb +5 -5
  294. data/lib/rubydb/replication/fencing.rb +63 -63
  295. data/lib/rubydb/replication/primary.rb +461 -450
  296. data/lib/rubydb/replication/replica.rb +382 -384
  297. data/lib/rubydb/replication/replication_log.rb +194 -200
  298. data/lib/rubydb/replication/replication_manager.rb +307 -308
  299. data/lib/rubydb/replication/replication_slot.rb +293 -295
  300. data/lib/rubydb/replication/replication_stream.rb +198 -201
  301. data/lib/rubydb/rubydb.rb +564 -560
  302. data/lib/rubydb/security/access_control.rb +252 -254
  303. data/lib/rubydb/security/audit_log.rb +209 -213
  304. data/lib/rubydb/security/authentication.rb +302 -302
  305. data/lib/rubydb/security/authorization.rb +282 -282
  306. data/lib/rubydb/security/credentials.rb +192 -196
  307. data/lib/rubydb/security/password.rb +205 -215
  308. data/lib/rubydb/security/permissions.rb +74 -74
  309. data/lib/rubydb/security/role.rb +99 -101
  310. data/lib/rubydb/security/user.rb +86 -86
  311. data/lib/rubydb/server/connection.rb +383 -366
  312. data/lib/rubydb/server/connection_pool.rb +193 -193
  313. data/lib/rubydb/server/lifecycle.rb +227 -228
  314. data/lib/rubydb/server/listener.rb +139 -136
  315. data/lib/rubydb/server/request_handler.rb +277 -276
  316. data/lib/rubydb/server/server.rb +363 -364
  317. data/lib/rubydb/server/session.rb +371 -369
  318. data/lib/rubydb/server/worker.rb +206 -210
  319. data/lib/rubydb/server/worker_pool.rb +168 -168
  320. data/lib/rubydb/sql/ast/alter_table.rb +169 -169
  321. data/lib/rubydb/sql/ast/begin_transaction.rb +47 -47
  322. data/lib/rubydb/sql/ast/commit.rb +37 -37
  323. data/lib/rubydb/sql/ast/constraint.rb +92 -83
  324. data/lib/rubydb/sql/ast/create_database.rb +41 -41
  325. data/lib/rubydb/sql/ast/create_index.rb +61 -61
  326. data/lib/rubydb/sql/ast/create_schema.rb +52 -52
  327. data/lib/rubydb/sql/ast/create_table.rb +187 -187
  328. data/lib/rubydb/sql/ast/delete.rb +54 -54
  329. data/lib/rubydb/sql/ast/drop_database.rb +41 -41
  330. data/lib/rubydb/sql/ast/drop_index.rb +41 -41
  331. data/lib/rubydb/sql/ast/drop_schema.rb +49 -49
  332. data/lib/rubydb/sql/ast/drop_table.rb +49 -49
  333. data/lib/rubydb/sql/ast/explain.rb +64 -64
  334. data/lib/rubydb/sql/ast/expression.rb +617 -604
  335. data/lib/rubydb/sql/ast/insert.rb +66 -66
  336. data/lib/rubydb/sql/ast/node.rb +42 -42
  337. data/lib/rubydb/sql/ast/rollback.rb +63 -63
  338. data/lib/rubydb/sql/ast/savepoint.rb +59 -59
  339. data/lib/rubydb/sql/ast/select.rb +88 -88
  340. data/lib/rubydb/sql/ast/set_operation.rb +22 -20
  341. data/lib/rubydb/sql/ast/trigger.rb +35 -29
  342. data/lib/rubydb/sql/ast/update.rb +88 -88
  343. data/lib/rubydb/sql/ast/vacuum.rb +19 -19
  344. data/lib/rubydb/sql/ast/view.rb +38 -32
  345. data/lib/rubydb/sql/ast/with.rb +32 -32
  346. data/lib/rubydb/sql/grammar.rb +86 -86
  347. data/lib/rubydb/sql/keywords.rb +156 -156
  348. data/lib/rubydb/sql/lexer.rb +209 -214
  349. data/lib/rubydb/sql/operators.rb +100 -100
  350. data/lib/rubydb/sql/parser.rb +1167 -1170
  351. data/lib/rubydb/sql/planner/analyzer.rb +283 -302
  352. data/lib/rubydb/sql/planner/binder.rb +537 -550
  353. data/lib/rubydb/sql/planner/type_checker.rb +427 -431
  354. data/lib/rubydb/sql/token.rb +210 -210
  355. data/lib/rubydb/storage/buffer_frame.rb +44 -44
  356. data/lib/rubydb/storage/buffer_pool.rb +155 -155
  357. data/lib/rubydb/storage/database_lock.rb +74 -74
  358. data/lib/rubydb/storage/deserializer.rb +332 -342
  359. data/lib/rubydb/storage/engine.rb +2347 -2330
  360. data/lib/rubydb/storage/file_manager.rb +191 -187
  361. data/lib/rubydb/storage/free_space_map.rb +79 -81
  362. data/lib/rubydb/storage/page.rb +92 -94
  363. data/lib/rubydb/storage/page_allocator.rb +852 -855
  364. data/lib/rubydb/storage/page_header.rb +63 -67
  365. data/lib/rubydb/storage/page_manager.rb +127 -131
  366. data/lib/rubydb/storage/record.rb +58 -58
  367. data/lib/rubydb/storage/row.rb +78 -78
  368. data/lib/rubydb/storage/serializer.rb +51 -51
  369. data/lib/rubydb/storage/storage_layout.rb +151 -151
  370. data/lib/rubydb/storage/storage_manager.rb +114 -114
  371. data/lib/rubydb/storage/tuple.rb +458 -461
  372. data/lib/rubydb/storage/visibility_map.rb +964 -973
  373. data/lib/rubydb/transactions/commit_manager.rb +219 -220
  374. data/lib/rubydb/transactions/isolation.rb +98 -98
  375. data/lib/rubydb/transactions/lock.rb +76 -76
  376. data/lib/rubydb/transactions/lock_manager.rb +359 -362
  377. data/lib/rubydb/transactions/savepoint.rb +142 -143
  378. data/lib/rubydb/transactions/transaction.rb +214 -215
  379. data/lib/rubydb/transactions/transaction_id.rb +84 -84
  380. data/lib/rubydb/transactions/transaction_log.rb +256 -257
  381. data/lib/rubydb/transactions/transaction_manager.rb +434 -435
  382. data/lib/rubydb/types/bigint.rb +36 -36
  383. data/lib/rubydb/types/blob.rb +37 -37
  384. data/lib/rubydb/types/boolean.rb +34 -34
  385. data/lib/rubydb/types/date.rb +39 -39
  386. data/lib/rubydb/types/decimal.rb +48 -48
  387. data/lib/rubydb/types/float.rb +34 -34
  388. data/lib/rubydb/types/integer.rb +36 -36
  389. data/lib/rubydb/types/json.rb +41 -41
  390. data/lib/rubydb/types/null.rb +34 -34
  391. data/lib/rubydb/types/smallint.rb +36 -36
  392. data/lib/rubydb/types/text.rb +37 -37
  393. data/lib/rubydb/types/time.rb +46 -46
  394. data/lib/rubydb/types/timestamp.rb +39 -39
  395. data/lib/rubydb/types/type.rb +119 -119
  396. data/lib/rubydb/types/uuid.rb +47 -47
  397. data/lib/rubydb/types/varchar.rb +37 -37
  398. data/lib/rubydb/version.rb +32 -32
  399. data/lib/rubydb/wal/archive.rb +190 -193
  400. data/lib/rubydb/wal/checkpoint.rb +181 -183
  401. data/lib/rubydb/wal/lsn.rb +94 -94
  402. data/lib/rubydb/wal/reader.rb +259 -260
  403. data/lib/rubydb/wal/record.rb +105 -105
  404. data/lib/rubydb/wal/segment.rb +193 -193
  405. data/lib/rubydb/wal/wal.rb +480 -452
  406. data/lib/rubydb/wal/writer.rb +236 -236
  407. data/lib/rubydb.rb +7 -7
  408. data/packaging/docker/docker-compose.failover.yml +43 -43
  409. data/packaging/homebrew/rubydb.rb +19 -19
  410. data/rubydb.gemspec +60 -57
  411. data/scripts/benchmark +7 -7
  412. data/scripts/durability_drill +37 -37
  413. data/scripts/fuzz +63 -63
  414. data/scripts/release +45 -40
  415. data/scripts/release_check +43 -43
  416. data/scripts/replication_failover_drill +268 -250
  417. data/scripts/replication_network_failover_drill +287 -255
  418. data/scripts/restore_drill +45 -45
  419. data/scripts/security +45 -0
  420. metadata +54 -1
@@ -1,384 +1,382 @@
1
- # frozen_string_literal: true
2
-
3
- require "socket"
4
- require "time"
5
- require "monitor"
6
- require "json"
7
- require "fileutils"
8
-
9
- module RubyDB
10
- module Replication
11
- # Replica - Replica database server
12
- class Replica
13
- attr_reader :config, :engine, :primary_host, :primary_port
14
- attr_reader :stats
15
-
16
- # Replica states
17
- STATE_INIT = :init
18
- STATE_CONNECTING = :connecting
19
- STATE_STREAMING = :streaming
20
- STATE_REPLAYING = :replaying
21
- STATE_CATCHING_UP = :catching_up
22
- STATE_SYNCED = :synced
23
- STATE_DISCONNECTED = :disconnected
24
- STATE_FAILED = :failed
25
-
26
- def initialize(engine, config = {})
27
- @engine = engine
28
- @config = config
29
- @primary_host = config[:primary_host] || "localhost"
30
- @primary_port = config[:primary_port] || 7433
31
- @replication_port = config[:replication_port] || 7434
32
- @state = STATE_INIT
33
- @connection = nil
34
- @replication_stream = nil
35
- @receive_buffer = +""
36
- @max_frame_size = config[:max_replication_frame_bytes] || 1_048_576
37
- @last_received_lsn = nil
38
- @last_replayed_lsn = nil
39
- @catchup_start_time = nil
40
- @stats = {
41
- bytes_received: 0,
42
- transactions_replayed: 0,
43
- replication_lag_ms: 0,
44
- reconnect_attempts: 0,
45
- last_connect_time: nil,
46
- last_disconnect_time: nil,
47
- total_uptime_ms: 0,
48
- last_error: nil
49
- }
50
- @lock = Monitor.new
51
- @running = false
52
- @replication_thread = nil
53
- @recovery = nil
54
- @retry_interval = config[:retry_interval] || 5
55
- @max_retry_attempts = config[:max_retry_attempts] || 10
56
- @heartbeat_interval = config[:heartbeat_interval] || 5
57
- @retry_count = 0
58
- @state_path = config[:state_path] || "#{@engine.path}.replica_state.json"
59
- @state_write_lock = Mutex.new
60
- @state_write_sequence = 0
61
- load_state
62
- @engine.set_replication_read_only(true) if @engine.respond_to?(:set_replication_read_only)
63
- end
64
-
65
- def start
66
- @lock.synchronize do
67
- return if @running
68
-
69
- @running = true
70
- @state = STATE_CONNECTING
71
- @replication_thread = Thread.new { replication_loop }
72
-
73
- puts "Replica started, connecting to primary at #{@primary_host}:#{@replication_port}"
74
- true
75
- end
76
- end
77
-
78
- def stop
79
- thread, connection = @lock.synchronize do
80
- return unless @running
81
-
82
- @running = false
83
- @state = STATE_DISCONNECTED
84
- [@replication_thread, @connection]
85
- end
86
-
87
- # Closing the socket wakes a blocked read. Let an in-flight frame finish
88
- # its durable state publication before joining the worker; killing it can
89
- # interrupt File.open/rename and leave replica state ambiguous.
90
- connection&.close
91
- thread&.join(5)
92
- if thread&.alive?
93
- thread.kill
94
- thread.join
95
- end
96
-
97
- @lock.synchronize do
98
- @connection = nil
99
- @replication_thread = nil
100
- @stats[:last_disconnect_time] = Time.now
101
- end
102
- true
103
- end
104
-
105
- def running?
106
- @lock.synchronize { @running }
107
- end
108
-
109
- def promote_to_primary(recovery_point: nil)
110
- @lock.synchronize do
111
- validate_promotion!(recovery_point)
112
- end
113
-
114
- # Stop outside the monitor so the worker can finish without waiting on
115
- # the promotion caller's lock.
116
- stop
117
-
118
- # Promotion is an explicit operator action. Re-enable ordinary
119
- # mutations only after the replication stream has been stopped.
120
- @engine.set_replication_read_only(false) if @engine.respond_to?(:set_replication_read_only)
121
- @lock.synchronize { @state = STATE_SYNCED }
122
- true
123
- end
124
-
125
- def replay_transaction(transaction_data, lsn = nil)
126
- @lock.synchronize do
127
- # Apply transaction to engine
128
- @engine.apply_transaction(transaction_data)
129
- @stats[:transactions_replayed] += 1
130
- @last_replayed_lsn = lsn || transaction_data[:lsn]
131
- persist_state
132
-
133
- true
134
- end
135
- end
136
-
137
- def replication_status
138
- @lock.synchronize do
139
- {
140
- state: @state,
141
- primary_host: @primary_host,
142
- primary_port: @primary_port,
143
- last_received_lsn: @last_received_lsn,
144
- last_replayed_lsn: @last_replayed_lsn,
145
- lag_ms: @stats[:replication_lag_ms],
146
- bytes_received: @stats[:bytes_received],
147
- transactions_replayed: @stats[:transactions_replayed],
148
- connected: @state == STATE_STREAMING || @state == STATE_SYNCED
149
- }
150
- end
151
- end
152
-
153
- private
154
-
155
- def replication_loop
156
- while @running
157
- begin
158
- case @state
159
- when STATE_INIT, STATE_CONNECTING
160
- connect_to_primary
161
- when STATE_STREAMING
162
- stream_replication
163
- when STATE_CATCHING_UP
164
- catch_up
165
- when STATE_SYNCED
166
- sleep(1)
167
- when STATE_DISCONNECTED, STATE_FAILED
168
- reconnect
169
- end
170
- rescue => e
171
- @stats[:last_error] = "#{e.class}: #{e.message}"
172
- @state = STATE_FAILED
173
- @stats[:reconnect_attempts] += 1
174
- sleep(@retry_interval)
175
- end
176
- end
177
- end
178
-
179
- def connect_to_primary
180
- @lock.synchronize do
181
- begin
182
- @connection = TCPSocket.new(@primary_host, @replication_port)
183
- @state = STATE_CONNECTING
184
- @stats[:last_connect_time] = Time.now
185
-
186
- # Send replica handshake
187
- handshake = {
188
- type: "replica_handshake",
189
- replica_id: @config[:replica_id] || "replica_#{Process.pid}",
190
- protocol_version: 1,
191
- wal_position: @last_replayed_lsn || 0,
192
- auth_token: @config[:replication_auth_token] || @config[:auth_token]
193
- }
194
-
195
- @connection.write(JSON.generate(handshake) + "\n")
196
- response = JSON.parse(@connection.readline)
197
-
198
- if response["success"]
199
- @state = STATE_STREAMING
200
- @stats[:reconnect_attempts] = 0
201
- @retry_count = 0
202
- puts "Connected to primary as replica"
203
- else
204
- @state = STATE_FAILED
205
- end
206
-
207
- rescue => e
208
- @stats[:last_error] = "#{e.class}: #{e.message}"
209
- @state = STATE_FAILED
210
- @stats[:reconnect_attempts] += 1
211
- raise
212
- end
213
- end
214
- end
215
-
216
- def stream_replication
217
- while @running && @state == STATE_STREAMING
218
- begin
219
- # Read replication data
220
- if @connection
221
- ready = IO.select([@connection], nil, nil, @heartbeat_interval)
222
- unless ready
223
- @connection.write(
224
- JSON.generate(
225
- type: "heartbeat",
226
- wal_position: @last_replayed_lsn || 0
227
- ) + "\n"
228
- )
229
- @connection.flush
230
- next
231
- end
232
-
233
- data = @connection.readpartial(4096)
234
- @stats[:bytes_received] += data.bytesize
235
-
236
- # Process replication data
237
- process_replication_data(data)
238
- else
239
- @state = STATE_DISCONNECTED
240
- break
241
- end
242
- rescue EOFError, Errno::ECONNRESET
243
- @state = STATE_DISCONNECTED
244
- break
245
- rescue => e
246
- @state = STATE_FAILED
247
- break
248
- end
249
- end
250
- end
251
-
252
- def process_replication_data(data)
253
- @receive_buffer << data.to_s
254
- if @receive_buffer.bytesize > @max_frame_size
255
- raise RubyDB::ReplicationError, "Replication frame exceeds #{@max_frame_size} bytes"
256
- end
257
-
258
- while (newline = @receive_buffer.index("\n"))
259
- line = @receive_buffer.slice!(0, newline + 1).strip
260
- next if line.empty?
261
-
262
- message = JSON.parse(line, symbolize_names: true)
263
- if message[:type].to_s == "replication_bootstrap"
264
- apply_bootstrap_schema(message[:schema].to_s)
265
- next
266
- end
267
- next unless message[:type].to_s == "replication_data"
268
-
269
- Array(message[:data]).each do |entry|
270
- next if entry[:lsn] && @last_replayed_lsn && entry[:lsn] <= @last_replayed_lsn
271
- replay_transaction(entry[:data] || entry, entry[:lsn])
272
- @last_received_lsn = entry[:lsn]
273
- persist_state
274
- @connection&.write(JSON.generate(type: "ack", lsn: @last_received_lsn) + "\n")
275
- end
276
- end
277
- end
278
-
279
- def catch_up
280
- @state = if @last_received_lsn && @last_received_lsn == @last_replayed_lsn
281
- STATE_SYNCED
282
- else
283
- STATE_STREAMING
284
- end
285
- end
286
-
287
- def apply_bootstrap_schema(schema)
288
- return if schema.empty? || !@engine.respond_to?(:with_replication_apply)
289
-
290
- @engine.with_replication_apply do
291
- statements = RubyDB::SQL::Parser.new(RubyDB::SQL::Lexer.new(schema).tokenize).parse
292
- statements.each do |statement|
293
- plan = RubyDB::Execution::Planner.new(@engine).plan(statement)
294
- RubyDB::Execution::Executor.new(@engine).execute(plan)
295
- end
296
- end
297
- rescue StandardError => error
298
- raise RubyDB::ReplicationError, "Replica bootstrap failed: #{error.message}"
299
- end
300
-
301
- def load_state
302
- return unless File.file?(@state_path)
303
- state = JSON.parse(File.read(@state_path), symbolize_names: true)
304
- @last_received_lsn = state[:last_received_lsn]
305
- @last_replayed_lsn = state[:last_replayed_lsn]
306
- rescue JSON::ParserError => error
307
- raise RubyDB::ReplicationError, "Invalid replica state #{@state_path}: #{error.message}"
308
- end
309
-
310
- def persist_state
311
- @state_write_lock.synchronize do
312
- FileUtils.mkdir_p(File.dirname(@state_path))
313
- @state_write_sequence += 1
314
- temporary = "#{@state_path}.tmp-#{Process.pid}-#{Thread.current.object_id}-#{@state_write_sequence}"
315
- payload = JSON.generate(
316
- last_received_lsn: @last_received_lsn,
317
- last_replayed_lsn: @last_replayed_lsn,
318
- updated_at: Time.now.iso8601
319
- )
320
- File.open(temporary, "wb") do |file|
321
- file.write(payload)
322
- file.flush
323
- file.fsync
324
- end
325
- begin
326
- File.rename(temporary, @state_path)
327
- rescue Errno::EACCES, Errno::EPERM
328
- # Windows does not replace an existing destination with rename(2).
329
- # FileUtils performs the platform-appropriate forced move while
330
- # retaining the fsynced temporary payload as the source.
331
- raise unless Gem.win_platform? && File.file?(@state_path)
332
-
333
- FileUtils.mv(temporary, @state_path, force: true)
334
- end
335
- ensure
336
- File.delete(temporary) if defined?(temporary) && File.file?(temporary)
337
- end
338
- end
339
-
340
- def reconnect
341
- @retry_count += 1
342
-
343
- if @retry_count > @max_retry_attempts
344
- @state = STATE_FAILED
345
- return
346
- end
347
-
348
- sleep(@retry_interval * @retry_count)
349
- @state = STATE_CONNECTING
350
- end
351
-
352
- def disconnect
353
- @connection&.close
354
- @connection = nil
355
- @state = STATE_DISCONNECTED
356
- @stats[:last_disconnect_time] = Time.now
357
- end
358
-
359
- def validate_promotion!(recovery_point)
360
- promotable_state = [STATE_STREAMING, STATE_SYNCED, STATE_DISCONNECTED, STATE_CONNECTING].include?(@state)
361
- caught_up_after_disconnect = [STATE_DISCONNECTED, STATE_CONNECTING].include?(@state) &&
362
- @last_replayed_lsn &&
363
- @last_received_lsn == @last_replayed_lsn
364
- unless promotable_state && ([STATE_STREAMING, STATE_SYNCED].include?(@state) || caught_up_after_disconnect)
365
- raise ReplicationError, "Replica is not synchronized enough for promotion"
366
- end
367
-
368
- if @last_received_lsn != @last_replayed_lsn
369
- raise ReplicationError,
370
- "Replica has unapplied replication data (received=#{@last_received_lsn.inspect}, " \
371
- "replayed=#{@last_replayed_lsn.inspect})"
372
- end
373
-
374
- return true if recovery_point.nil?
375
-
376
- target = recovery_point.to_i
377
- replayed = @last_replayed_lsn.to_i
378
- raise ReplicationError, "Replica has not reached recovery point #{target}" if replayed < target
379
-
380
- true
381
- end
382
- end
383
- end
384
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "time"
5
+ require "monitor"
6
+ require "json"
7
+ require "fileutils"
8
+
9
+ module RubyDB
10
+ module Replication
11
+ # Replica - Replica database server
12
+ class Replica
13
+ attr_reader :config, :engine, :primary_host, :primary_port
14
+ attr_reader :stats
15
+
16
+ # Replica states
17
+ STATE_INIT = :init
18
+ STATE_CONNECTING = :connecting
19
+ STATE_STREAMING = :streaming
20
+ STATE_REPLAYING = :replaying
21
+ STATE_CATCHING_UP = :catching_up
22
+ STATE_SYNCED = :synced
23
+ STATE_DISCONNECTED = :disconnected
24
+ STATE_FAILED = :failed
25
+
26
+ def initialize(engine, config = {})
27
+ @engine = engine
28
+ @config = config
29
+ @primary_host = config[:primary_host] || "localhost"
30
+ @primary_port = config[:primary_port] || 7433
31
+ @replication_port = config[:replication_port] || 7434
32
+ @state = STATE_INIT
33
+ @connection = nil
34
+ @replication_stream = nil
35
+ @receive_buffer = +""
36
+ @max_frame_size = config[:max_replication_frame_bytes] || 1_048_576
37
+ @last_received_lsn = nil
38
+ @last_replayed_lsn = nil
39
+ @catchup_start_time = nil
40
+ @stats = {
41
+ bytes_received: 0,
42
+ transactions_replayed: 0,
43
+ replication_lag_ms: 0,
44
+ reconnect_attempts: 0,
45
+ last_connect_time: nil,
46
+ last_disconnect_time: nil,
47
+ total_uptime_ms: 0,
48
+ last_error: nil
49
+ }
50
+ @lock = Monitor.new
51
+ @running = false
52
+ @replication_thread = nil
53
+ @recovery = nil
54
+ @retry_interval = config[:retry_interval] || 5
55
+ @max_retry_attempts = config[:max_retry_attempts] || 10
56
+ @heartbeat_interval = config[:heartbeat_interval] || 5
57
+ @retry_count = 0
58
+ @state_path = config[:state_path] || "#{@engine.path}.replica_state.json"
59
+ @state_write_lock = Mutex.new
60
+ @state_write_sequence = 0
61
+ load_state
62
+ @engine.set_replication_read_only(true) if @engine.respond_to?(:set_replication_read_only)
63
+ end
64
+
65
+ def start
66
+ @lock.synchronize do
67
+ return if @running
68
+
69
+ @running = true
70
+ @state = STATE_CONNECTING
71
+ @replication_thread = Thread.new { replication_loop }
72
+
73
+ puts "Replica started, connecting to primary at #{@primary_host}:#{@replication_port}"
74
+ true
75
+ end
76
+ end
77
+
78
+ def stop
79
+ thread, connection = @lock.synchronize do
80
+ return unless @running
81
+
82
+ @running = false
83
+ @state = STATE_DISCONNECTED
84
+ [@replication_thread, @connection]
85
+ end
86
+
87
+ # Closing the socket wakes a blocked read. Let an in-flight frame finish
88
+ # its durable state publication before joining the worker; killing it can
89
+ # interrupt File.open/rename and leave replica state ambiguous.
90
+ connection&.close
91
+ thread&.join(5)
92
+ if thread&.alive?
93
+ thread.kill
94
+ thread.join
95
+ end
96
+
97
+ @lock.synchronize do
98
+ @connection = nil
99
+ @replication_thread = nil
100
+ @stats[:last_disconnect_time] = Time.now
101
+ end
102
+ true
103
+ end
104
+
105
+ def running?
106
+ @lock.synchronize { @running }
107
+ end
108
+
109
+ def promote_to_primary(recovery_point: nil)
110
+ @lock.synchronize do
111
+ validate_promotion!(recovery_point)
112
+ end
113
+
114
+ # Stop outside the monitor so the worker can finish without waiting on
115
+ # the promotion caller's lock.
116
+ stop
117
+
118
+ # Promotion is an explicit operator action. Re-enable ordinary
119
+ # mutations only after the replication stream has been stopped.
120
+ @engine.set_replication_read_only(false) if @engine.respond_to?(:set_replication_read_only)
121
+ @lock.synchronize { @state = STATE_SYNCED }
122
+ true
123
+ end
124
+
125
+ def replay_transaction(transaction_data, lsn = nil)
126
+ @lock.synchronize do
127
+ # Apply transaction to engine
128
+ @engine.apply_transaction(transaction_data)
129
+ @stats[:transactions_replayed] += 1
130
+ @last_replayed_lsn = lsn || transaction_data[:lsn]
131
+ persist_state
132
+
133
+ true
134
+ end
135
+ end
136
+
137
+ def replication_status
138
+ @lock.synchronize do
139
+ {
140
+ state: @state,
141
+ primary_host: @primary_host,
142
+ primary_port: @primary_port,
143
+ last_received_lsn: @last_received_lsn,
144
+ last_replayed_lsn: @last_replayed_lsn,
145
+ lag_ms: @stats[:replication_lag_ms],
146
+ bytes_received: @stats[:bytes_received],
147
+ transactions_replayed: @stats[:transactions_replayed],
148
+ connected: @state == STATE_STREAMING || @state == STATE_SYNCED
149
+ }
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ def replication_loop
156
+ while @running
157
+ begin
158
+ case @state
159
+ when STATE_INIT, STATE_CONNECTING
160
+ connect_to_primary
161
+ when STATE_STREAMING
162
+ stream_replication
163
+ when STATE_CATCHING_UP
164
+ catch_up
165
+ when STATE_SYNCED
166
+ sleep(1)
167
+ when STATE_DISCONNECTED, STATE_FAILED
168
+ reconnect
169
+ end
170
+ rescue => e
171
+ @stats[:last_error] = "#{e.class}: #{e.message}"
172
+ @state = STATE_FAILED
173
+ @stats[:reconnect_attempts] += 1
174
+ sleep(@retry_interval)
175
+ end
176
+ end
177
+ end
178
+
179
+ def connect_to_primary
180
+ @lock.synchronize do
181
+ @connection = TCPSocket.new(@primary_host, @replication_port)
182
+ @state = STATE_CONNECTING
183
+ @stats[:last_connect_time] = Time.now
184
+
185
+ # Send replica handshake
186
+ handshake = {
187
+ type: "replica_handshake",
188
+ replica_id: @config[:replica_id] || "replica_#{Process.pid}",
189
+ protocol_version: 1,
190
+ wal_position: @last_replayed_lsn || 0,
191
+ auth_token: @config[:replication_auth_token] || @config[:auth_token]
192
+ }
193
+
194
+ @connection.write(JSON.generate(handshake) + "\n")
195
+ response = JSON.parse(@connection.readline)
196
+
197
+ if response["success"]
198
+ @state = STATE_STREAMING
199
+ @stats[:reconnect_attempts] = 0
200
+ @retry_count = 0
201
+ puts "Connected to primary as replica"
202
+ else
203
+ @state = STATE_FAILED
204
+ end
205
+ rescue => e
206
+ @stats[:last_error] = "#{e.class}: #{e.message}"
207
+ @state = STATE_FAILED
208
+ @stats[:reconnect_attempts] += 1
209
+ raise
210
+ end
211
+ end
212
+
213
+ def stream_replication
214
+ while @running && @state == STATE_STREAMING
215
+ begin
216
+ # Read replication data
217
+ if @connection
218
+ ready = IO.select([@connection], nil, nil, @heartbeat_interval)
219
+ unless ready
220
+ @connection.write(
221
+ JSON.generate(
222
+ type: "heartbeat",
223
+ wal_position: @last_replayed_lsn || 0
224
+ ) + "\n"
225
+ )
226
+ @connection.flush
227
+ next
228
+ end
229
+
230
+ data = @connection.readpartial(4096)
231
+ @stats[:bytes_received] += data.bytesize
232
+
233
+ # Process replication data
234
+ process_replication_data(data)
235
+ else
236
+ @state = STATE_DISCONNECTED
237
+ break
238
+ end
239
+ rescue EOFError, Errno::ECONNRESET
240
+ @state = STATE_DISCONNECTED
241
+ break
242
+ rescue
243
+ @state = STATE_FAILED
244
+ break
245
+ end
246
+ end
247
+ end
248
+
249
+ def process_replication_data(data)
250
+ @receive_buffer << data.to_s
251
+ if @receive_buffer.bytesize > @max_frame_size
252
+ raise RubyDB::ReplicationError, "Replication frame exceeds #{@max_frame_size} bytes"
253
+ end
254
+
255
+ while (newline = @receive_buffer.index("\n"))
256
+ line = @receive_buffer.slice!(0, newline + 1).strip
257
+ next if line.empty?
258
+
259
+ message = JSON.parse(line, symbolize_names: true)
260
+ if message[:type].to_s == "replication_bootstrap"
261
+ apply_bootstrap_schema(message[:schema].to_s)
262
+ next
263
+ end
264
+ next unless message[:type].to_s == "replication_data"
265
+
266
+ Array(message[:data]).each do |entry|
267
+ next if entry[:lsn] && @last_replayed_lsn && entry[:lsn] <= @last_replayed_lsn
268
+ replay_transaction(entry[:data] || entry, entry[:lsn])
269
+ @last_received_lsn = entry[:lsn]
270
+ persist_state
271
+ @connection&.write(JSON.generate(type: "ack", lsn: @last_received_lsn) + "\n")
272
+ end
273
+ end
274
+ end
275
+
276
+ def catch_up
277
+ @state = if @last_received_lsn && @last_received_lsn == @last_replayed_lsn
278
+ STATE_SYNCED
279
+ else
280
+ STATE_STREAMING
281
+ end
282
+ end
283
+
284
+ def apply_bootstrap_schema(schema)
285
+ return if schema.empty? || !@engine.respond_to?(:with_replication_apply)
286
+
287
+ @engine.with_replication_apply do
288
+ statements = RubyDB::SQL::Parser.new(RubyDB::SQL::Lexer.new(schema).tokenize).parse
289
+ statements.each do |statement|
290
+ plan = RubyDB::Execution::Planner.new(@engine).plan(statement)
291
+ RubyDB::Execution::Executor.new(@engine).execute(plan)
292
+ end
293
+ end
294
+ rescue => error
295
+ raise RubyDB::ReplicationError, "Replica bootstrap failed: #{error.message}"
296
+ end
297
+
298
+ def load_state
299
+ return unless File.file?(@state_path)
300
+ state = JSON.parse(File.read(@state_path), symbolize_names: true)
301
+ @last_received_lsn = state[:last_received_lsn]
302
+ @last_replayed_lsn = state[:last_replayed_lsn]
303
+ rescue JSON::ParserError => error
304
+ raise RubyDB::ReplicationError, "Invalid replica state #{@state_path}: #{error.message}"
305
+ end
306
+
307
+ def persist_state
308
+ @state_write_lock.synchronize do
309
+ FileUtils.mkdir_p(File.dirname(@state_path))
310
+ @state_write_sequence += 1
311
+ temporary = "#{@state_path}.tmp-#{Process.pid}-#{Thread.current.object_id}-#{@state_write_sequence}"
312
+ payload = JSON.generate(
313
+ last_received_lsn: @last_received_lsn,
314
+ last_replayed_lsn: @last_replayed_lsn,
315
+ updated_at: Time.now.iso8601
316
+ )
317
+ File.open(temporary, "wb") do |file|
318
+ file.write(payload)
319
+ file.flush
320
+ file.fsync
321
+ end
322
+ begin
323
+ File.rename(temporary, @state_path)
324
+ rescue Errno::EACCES, Errno::EPERM
325
+ # Windows does not replace an existing destination with rename(2).
326
+ # FileUtils performs the platform-appropriate forced move while
327
+ # retaining the fsynced temporary payload as the source.
328
+ raise unless Gem.win_platform? && File.file?(@state_path)
329
+
330
+ FileUtils.mv(temporary, @state_path, force: true)
331
+ end
332
+ ensure
333
+ File.delete(temporary) if defined?(temporary) && File.file?(temporary)
334
+ end
335
+ end
336
+
337
+ def reconnect
338
+ @retry_count += 1
339
+
340
+ if @retry_count > @max_retry_attempts
341
+ @state = STATE_FAILED
342
+ return
343
+ end
344
+
345
+ sleep(@retry_interval * @retry_count)
346
+ @state = STATE_CONNECTING
347
+ end
348
+
349
+ def disconnect
350
+ @connection&.close
351
+ @connection = nil
352
+ @state = STATE_DISCONNECTED
353
+ @stats[:last_disconnect_time] = Time.now
354
+ end
355
+
356
+ def validate_promotion!(recovery_point)
357
+ promotable_state = [STATE_STREAMING, STATE_SYNCED, STATE_DISCONNECTED, STATE_CONNECTING].include?(@state)
358
+ caught_up_after_disconnect = [STATE_DISCONNECTED, STATE_CONNECTING].include?(@state) &&
359
+ @last_replayed_lsn &&
360
+ @last_received_lsn == @last_replayed_lsn
361
+ synchronized_state = [STATE_STREAMING, STATE_SYNCED].include?(@state) || caught_up_after_disconnect
362
+ unless promotable_state && synchronized_state
363
+ raise ReplicationError, "Replica is not synchronized enough for promotion"
364
+ end
365
+
366
+ if @last_received_lsn != @last_replayed_lsn
367
+ raise ReplicationError,
368
+ "Replica has unapplied replication data (received=#{@last_received_lsn.inspect}, " \
369
+ "replayed=#{@last_replayed_lsn.inspect})"
370
+ end
371
+
372
+ return true if recovery_point.nil?
373
+
374
+ target = recovery_point.to_i
375
+ replayed = @last_replayed_lsn.to_i
376
+ raise ReplicationError, "Replica has not reached recovery point #{target}" if replayed < target
377
+
378
+ true
379
+ end
380
+ end
381
+ end
382
+ end