rubydb 0.1.4 → 0.1.6

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 (469) 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 +16 -5
  14. data/.rubocop.yml +50 -44
  15. data/.standard.yml +9 -14
  16. data/ARCHITECTURE.md +21 -21
  17. data/CHANGELOG.md +57 -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 +168 -12
  24. data/ROADMAP.md +27 -27
  25. data/Rakefile +76 -71
  26. data/SECURITY.md +54 -54
  27. data/SUPPORT.md +14 -14
  28. data/accelerator/bin/SHA256SUMS +6 -0
  29. data/accelerator/bin/rubydb-accelerator-darwin-amd64 +0 -0
  30. data/accelerator/bin/rubydb-accelerator-darwin-arm64 +0 -0
  31. data/accelerator/bin/rubydb-accelerator-linux-amd64 +0 -0
  32. data/accelerator/bin/rubydb-accelerator-linux-arm64 +0 -0
  33. data/accelerator/bin/rubydb-accelerator-windows-amd64.exe +0 -0
  34. data/accelerator/bin/rubydb-accelerator-windows-arm64.exe +0 -0
  35. data/accelerator/cmd/rubydb-accelerator/main.go +11 -0
  36. data/accelerator/go.mod +3 -0
  37. data/accelerator/internal/execution/aggregate.go +94 -0
  38. data/accelerator/internal/execution/distinct.go +22 -0
  39. data/accelerator/internal/execution/filter.go +73 -0
  40. data/accelerator/internal/execution/join.go +79 -0
  41. data/accelerator/internal/execution/operators.go +167 -0
  42. data/accelerator/internal/execution/scan.go +20 -0
  43. data/accelerator/internal/execution/sort.go +62 -0
  44. data/accelerator/internal/execution/types.go +136 -0
  45. data/accelerator/internal/execution/value.go +67 -0
  46. data/accelerator/internal/memory/arena.go +47 -0
  47. data/accelerator/internal/memory/reuse.go +22 -0
  48. data/accelerator/internal/metrics/registry.go +67 -0
  49. data/accelerator/internal/parallel/bounded_queue.go +56 -0
  50. data/accelerator/internal/parallel/scheduler.go +47 -0
  51. data/accelerator/internal/parallel/worker_pool.go +53 -0
  52. data/accelerator/internal/protocol/cancellation.go +48 -0
  53. data/accelerator/internal/protocol/columnar.go +263 -0
  54. data/accelerator/internal/protocol/frame.go +187 -0
  55. data/accelerator/internal/runtime/worker.go +521 -0
  56. data/accelerator/internal/storage/page_reader.go +81 -0
  57. data/accelerator/internal/storage/snapshot_scan.go +539 -0
  58. data/accelerator/internal/wal/checksum.go +13 -0
  59. data/accelerator/internal/wal/compression.go +41 -0
  60. data/accelerator/internal/wal/group_commit.go +24 -0
  61. data/accelerator/internal/wal/record_encoder.go +40 -0
  62. data/adapters/activerecord/Gemfile +11 -11
  63. data/adapters/activerecord/README.md +8 -3
  64. data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +881 -879
  65. data/adapters/activerecord/rubydb-activerecord.gemspec +21 -21
  66. data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +143 -143
  67. data/adapters/ruby/README.md +18 -18
  68. data/adapters/sequel/README.md +11 -11
  69. data/config/monitoring/prometheus-alerts.yml +39 -39
  70. data/config/production.yml +36 -36
  71. data/docs/README.md +77 -71
  72. data/docs/architecture/concurrency.md +14 -14
  73. data/docs/architecture/current-state.md +125 -125
  74. data/docs/architecture/execution-engine.md +25 -25
  75. data/docs/architecture/go-accelerator.md +179 -0
  76. data/docs/architecture/indexes.md +19 -19
  77. data/docs/architecture/mvcc.md +19 -19
  78. data/docs/architecture/overview.md +13 -13
  79. data/docs/architecture/pages.md +11 -11
  80. data/docs/architecture/production-roadmap.md +82 -82
  81. data/docs/architecture/query-planner.md +20 -20
  82. data/docs/architecture/recovery.md +18 -18
  83. data/docs/architecture/sql-engine.md +12 -12
  84. data/docs/architecture/storage-engine.md +14 -14
  85. data/docs/architecture/transactions.md +10 -10
  86. data/docs/architecture/wal.md +28 -28
  87. data/docs/cli-cheatsheet.md +98 -98
  88. data/docs/cli.md +299 -275
  89. data/docs/contributing/architecture.md +9 -9
  90. data/docs/contributing/benchmarking.md +30 -14
  91. data/docs/contributing/development.md +16 -16
  92. data/docs/contributing/release-process.md +49 -49
  93. data/docs/contributing/testing.md +16 -16
  94. data/docs/debugging.md +229 -229
  95. data/docs/developer/branching.md +10 -10
  96. data/docs/developer/database-diff.md +10 -10
  97. data/docs/developer/local-development.md +49 -17
  98. data/docs/developer/snapshots.md +9 -9
  99. data/docs/developer/temporal-data.md +10 -10
  100. data/docs/developer-guide.md +297 -297
  101. data/docs/getting-started/first-database.md +16 -16
  102. data/docs/getting-started/first-query.md +13 -13
  103. data/docs/getting-started/installation.md +19 -19
  104. data/docs/getting-started/local-to-production.md +300 -300
  105. data/docs/getting-started/quickstart.md +17 -17
  106. data/docs/getting-started/rails.md +16 -16
  107. data/docs/hardening_backlog.md +93 -93
  108. data/docs/lessons-learned.md +112 -112
  109. data/docs/operations/backups.md +33 -33
  110. data/docs/operations/disaster-recovery.md +31 -31
  111. data/docs/operations/failover.md +30 -30
  112. data/docs/operations/monitoring.md +25 -25
  113. data/docs/operations/production-guide.md +295 -295
  114. data/docs/operations/production-runbook.md +45 -45
  115. data/docs/operations/replication.md +33 -33
  116. data/docs/operations/restore.md +6 -6
  117. data/docs/operations/runbook.md +34 -34
  118. data/docs/operations/upgrades.md +14 -14
  119. data/docs/operations/workload-testing.md +17 -17
  120. data/docs/production-readiness.md +118 -118
  121. data/docs/production_validation.md +150 -150
  122. data/docs/rails/active-record.md +11 -11
  123. data/docs/rails/compatibility-guide.md +90 -90
  124. data/docs/rails/database-yml.md +92 -92
  125. data/docs/rails/installation.md +17 -17
  126. data/docs/rails/migrations.md +17 -17
  127. data/docs/rails/production.md +82 -82
  128. data/docs/rails/troubleshooting.md +18 -18
  129. data/docs/release.md +25 -25
  130. data/docs/server/architecture.md +10 -10
  131. data/docs/server/authentication.md +10 -10
  132. data/docs/server/configuration.md +16 -16
  133. data/docs/server/connection-pooling.md +10 -10
  134. data/docs/server/deployment.md +10 -10
  135. data/docs/server/protocol.md +12 -12
  136. data/docs/sql/compatibility-guide.md +82 -82
  137. data/docs/sql/compatibility.md +39 -39
  138. data/docs/sql/data-types.md +10 -10
  139. data/docs/sql/functions.md +9 -9
  140. data/docs/sql/joins.md +9 -9
  141. data/docs/sql/operators.md +9 -9
  142. data/docs/sql/sqlite-compatibility.md +21 -21
  143. data/docs/sql/syntax.md +10 -10
  144. data/docs/sql/transactions.md +10 -10
  145. data/docs/troubleshooting.md +244 -244
  146. data/lessons/01-foundations.md +73 -0
  147. data/lessons/02-local-development.md +121 -0
  148. data/lessons/03-embedded-rubydb.md +99 -0
  149. data/lessons/04-rails-complex-apps.md +138 -0
  150. data/lessons/05-rubydb-production-server.md +237 -0
  151. data/lessons/06-postgresql-massive-apps.md +96 -0
  152. data/lessons/07-hybrid-microservices.md +179 -0
  153. data/lessons/08-migrations-backups-recovery.md +86 -0
  154. data/lessons/09-observability-security-scale.md +87 -0
  155. data/lessons/10-release-readiness.md +192 -0
  156. data/lessons/11-community-adapter.md +323 -0
  157. data/lessons/12-rails-ecommerce-pressure.md +263 -0
  158. data/lib/rubydb/accelerator/client.rb +451 -0
  159. data/lib/rubydb/accelerator/error.rb +22 -0
  160. data/lib/rubydb/accelerator/manager.rb +606 -0
  161. data/lib/rubydb/accelerator.rb +13 -0
  162. data/lib/rubydb/backup/archive.rb +332 -334
  163. data/lib/rubydb/backup/backup.rb +400 -401
  164. data/lib/rubydb/backup/incremental.rb +349 -353
  165. data/lib/rubydb/backup/restore.rb +289 -290
  166. data/lib/rubydb/backup/snapshot.rb +265 -267
  167. data/lib/rubydb/backup/verification.rb +276 -279
  168. data/lib/rubydb/branching/branch.rb +181 -181
  169. data/lib/rubydb/branching/branch_manager.rb +307 -311
  170. data/lib/rubydb/branching/branch_metadata.rb +140 -140
  171. data/lib/rubydb/branching/checkout.rb +165 -166
  172. data/lib/rubydb/branching/copy_on_write.rb +272 -272
  173. data/lib/rubydb/branching/diff.rb +137 -138
  174. data/lib/rubydb/branching/merge.rb +282 -285
  175. data/lib/rubydb/build_info.rb +15 -15
  176. data/lib/rubydb/catalog/catalog.rb +391 -391
  177. data/lib/rubydb/catalog/column.rb +112 -112
  178. data/lib/rubydb/catalog/constraint.rb +180 -180
  179. data/lib/rubydb/catalog/database.rb +184 -184
  180. data/lib/rubydb/catalog/index.rb +97 -97
  181. data/lib/rubydb/catalog/schema.rb +103 -103
  182. data/lib/rubydb/catalog/sequence.rb +90 -90
  183. data/lib/rubydb/catalog/system_catalog.rb +698 -698
  184. data/lib/rubydb/catalog/table.rb +178 -178
  185. data/lib/rubydb/catalog/trigger.rb +102 -102
  186. data/lib/rubydb/catalog/view.rb +66 -66
  187. data/lib/rubydb/cli/application.rb +168 -163
  188. data/lib/rubydb/cli/commands/accelerator.rb +72 -0
  189. data/lib/rubydb/cli/commands/backup.rb +80 -81
  190. data/lib/rubydb/cli/commands/branch.rb +72 -72
  191. data/lib/rubydb/cli/commands/checkout.rb +54 -54
  192. data/lib/rubydb/cli/commands/create.rb +58 -58
  193. data/lib/rubydb/cli/commands/diff.rb +76 -77
  194. data/lib/rubydb/cli/commands/doctor.rb +77 -74
  195. data/lib/rubydb/cli/commands/drop.rb +57 -57
  196. data/lib/rubydb/cli/commands/init.rb +101 -102
  197. data/lib/rubydb/cli/commands/inspect.rb +95 -95
  198. data/lib/rubydb/cli/commands/merge.rb +63 -63
  199. data/lib/rubydb/cli/commands/migrate.rb +62 -62
  200. data/lib/rubydb/cli/commands/restart.rb +42 -39
  201. data/lib/rubydb/cli/commands/restore.rb +121 -121
  202. data/lib/rubydb/cli/commands/shell.rb +365 -365
  203. data/lib/rubydb/cli/commands/snapshot.rb +79 -79
  204. data/lib/rubydb/cli/commands/start.rb +88 -82
  205. data/lib/rubydb/cli/commands/status.rb +96 -92
  206. data/lib/rubydb/cli/commands/stop.rb +47 -47
  207. data/lib/rubydb/cli/commands/vacuum.rb +58 -58
  208. data/lib/rubydb/cli/formatter.rb +221 -221
  209. data/lib/rubydb/cli/output.rb +168 -168
  210. data/lib/rubydb/client/client.rb +309 -304
  211. data/lib/rubydb/client/connection.rb +429 -415
  212. data/lib/rubydb/client/connection_pool.rb +168 -168
  213. data/lib/rubydb/client/connection_url.rb +96 -96
  214. data/lib/rubydb/client/prepared_statement.rb +60 -60
  215. data/lib/rubydb/client/result.rb +127 -123
  216. data/lib/rubydb/client/statement.rb +52 -52
  217. data/lib/rubydb/client/transaction.rb +130 -130
  218. data/lib/rubydb/concurrency/concurrency.rb +19 -19
  219. data/lib/rubydb/concurrency/deadlock_detector.rb +148 -150
  220. data/lib/rubydb/concurrency/latch.rb +101 -101
  221. data/lib/rubydb/concurrency/lock_graph.rb +163 -165
  222. data/lib/rubydb/concurrency/mutex.rb +181 -183
  223. data/lib/rubydb/concurrency/rw_lock.rb +180 -180
  224. data/lib/rubydb/concurrency/scheduler.rb +248 -250
  225. data/lib/rubydb/concurrency/worker_pool.rb +145 -143
  226. data/lib/rubydb/configuration/config.rb +170 -170
  227. data/lib/rubydb/configuration/defaults.rb +191 -179
  228. data/lib/rubydb/configuration/environment.rb +152 -152
  229. data/lib/rubydb/configuration/parser.rb +185 -185
  230. data/lib/rubydb/configuration/validation.rb +228 -221
  231. data/lib/rubydb/constants.rb +74 -74
  232. data/lib/rubydb/constraints/check.rb +181 -181
  233. data/lib/rubydb/constraints/constraint.rb +101 -101
  234. data/lib/rubydb/constraints/foreign_key.rb +130 -130
  235. data/lib/rubydb/constraints/not_null.rb +64 -64
  236. data/lib/rubydb/constraints/primary_key.rb +99 -99
  237. data/lib/rubydb/constraints/unique.rb +106 -108
  238. data/lib/rubydb/constraints/validator.rb +349 -350
  239. data/lib/rubydb/errors/authentication_error.rb +10 -10
  240. data/lib/rubydb/errors/authorization_error.rb +23 -23
  241. data/lib/rubydb/errors/client_error.rb +10 -10
  242. data/lib/rubydb/errors/configuration_error.rb +10 -10
  243. data/lib/rubydb/errors/connection_error.rb +10 -10
  244. data/lib/rubydb/errors/constraint_error.rb +23 -23
  245. data/lib/rubydb/errors/corruption_error.rb +10 -10
  246. data/lib/rubydb/errors/database_error.rb +10 -10
  247. data/lib/rubydb/errors/error.rb +20 -20
  248. data/lib/rubydb/errors/execution_error.rb +10 -10
  249. data/lib/rubydb/errors/parser_error.rb +10 -10
  250. data/lib/rubydb/errors/recovery_error.rb +10 -10
  251. data/lib/rubydb/errors/replication_error.rb +10 -10
  252. data/lib/rubydb/errors/server_error.rb +6 -6
  253. data/lib/rubydb/errors/storage_error.rb +10 -10
  254. data/lib/rubydb/errors/transaction_error.rb +10 -10
  255. data/lib/rubydb/execution/accelerator_dispatch.rb +30 -0
  256. data/lib/rubydb/execution/aggregate_executor.rb +134 -138
  257. data/lib/rubydb/execution/cost_model.rb +72 -0
  258. data/lib/rubydb/execution/delete_executor.rb +110 -112
  259. data/lib/rubydb/execution/distinct_executor.rb +131 -135
  260. data/lib/rubydb/execution/executor.rb +1544 -1188
  261. data/lib/rubydb/execution/expression.rb +191 -193
  262. data/lib/rubydb/execution/index_scan.rb +142 -142
  263. data/lib/rubydb/execution/insert_executor.rb +215 -217
  264. data/lib/rubydb/execution/join_executor.rb +243 -249
  265. data/lib/rubydb/execution/limit_executor.rb +83 -85
  266. data/lib/rubydb/execution/operator_selection.rb +57 -0
  267. data/lib/rubydb/execution/optimizer.rb +227 -215
  268. data/lib/rubydb/execution/physical_plan.rb +47 -0
  269. data/lib/rubydb/execution/plan.rb +355 -353
  270. data/lib/rubydb/execution/planner.rb +508 -536
  271. data/lib/rubydb/execution/predicate.rb +235 -235
  272. data/lib/rubydb/execution/scan.rb +49 -49
  273. data/lib/rubydb/execution/sequential_scan.rb +63 -63
  274. data/lib/rubydb/execution/sort_executor.rb +194 -185
  275. data/lib/rubydb/execution/update_executor.rb +160 -162
  276. data/lib/rubydb/functions/aggregate.rb +70 -70
  277. data/lib/rubydb/functions/date_functions.rb +274 -278
  278. data/lib/rubydb/functions/function.rb +85 -85
  279. data/lib/rubydb/functions/json_functions.rb +231 -215
  280. data/lib/rubydb/functions/numeric_functions.rb +346 -346
  281. data/lib/rubydb/functions/scalar.rb +52 -52
  282. data/lib/rubydb/functions/string_functions.rb +383 -383
  283. data/lib/rubydb/functions/system_functions.rb +258 -246
  284. data/lib/rubydb/history/as_of.rb +238 -238
  285. data/lib/rubydb/history/change.rb +105 -105
  286. data/lib/rubydb/history/history.rb +131 -131
  287. data/lib/rubydb/history/history_manager.rb +228 -229
  288. data/lib/rubydb/history/temporal_query.rb +202 -202
  289. data/lib/rubydb/history/timeline.rb +144 -144
  290. data/lib/rubydb/indexes/btree.rb +215 -186
  291. data/lib/rubydb/indexes/btree_cursor.rb +258 -258
  292. data/lib/rubydb/indexes/btree_node.rb +384 -385
  293. data/lib/rubydb/indexes/hash_index.rb +150 -150
  294. data/lib/rubydb/indexes/index.rb +71 -71
  295. data/lib/rubydb/indexes/index_manager.rb +408 -406
  296. data/lib/rubydb/indexes/index_scan.rb +466 -470
  297. data/lib/rubydb/migrations/migration.rb +253 -254
  298. data/lib/rubydb/migrations/migration_lock.rb +146 -146
  299. data/lib/rubydb/migrations/migration_manager.rb +187 -176
  300. data/lib/rubydb/migrations/migration_version.rb +71 -71
  301. data/lib/rubydb/migrations/schema_diff.rb +211 -211
  302. data/lib/rubydb/migrations/schema_version.rb +64 -64
  303. data/lib/rubydb/monitoring/events.rb +155 -160
  304. data/lib/rubydb/monitoring/health.rb +216 -222
  305. data/lib/rubydb/monitoring/logger.rb +188 -193
  306. data/lib/rubydb/monitoring/metrics.rb +363 -359
  307. data/lib/rubydb/monitoring/performance.rb +176 -176
  308. data/lib/rubydb/monitoring/statistics.rb +168 -170
  309. data/lib/rubydb/mvcc/garbage_collector.rb +199 -199
  310. data/lib/rubydb/mvcc/mvcc.rb +16 -16
  311. data/lib/rubydb/mvcc/snapshot.rb +146 -147
  312. data/lib/rubydb/mvcc/vacuum.rb +180 -180
  313. data/lib/rubydb/mvcc/version.rb +106 -106
  314. data/lib/rubydb/mvcc/version_store.rb +396 -398
  315. data/lib/rubydb/mvcc/visibility.rb +107 -109
  316. data/lib/rubydb/protocol/capabilities.rb +125 -125
  317. data/lib/rubydb/protocol/decoder.rb +142 -145
  318. data/lib/rubydb/protocol/encoder.rb +131 -136
  319. data/lib/rubydb/protocol/handshake.rb +306 -305
  320. data/lib/rubydb/protocol/message.rb +121 -121
  321. data/lib/rubydb/protocol/parameter_binder.rb +101 -0
  322. data/lib/rubydb/protocol/protocol.rb +276 -277
  323. data/lib/rubydb/protocol/version.rb +54 -54
  324. data/lib/rubydb/rails/adapter.rb +245 -239
  325. data/lib/rubydb/rails/connection.rb +312 -314
  326. data/lib/rubydb/rails/database_statements.rb +122 -122
  327. data/lib/rubydb/rails/migration.rb +131 -131
  328. data/lib/rubydb/rails/quoting.rb +109 -109
  329. data/lib/rubydb/rails/result.rb +117 -117
  330. data/lib/rubydb/rails/schema_statements.rb +339 -339
  331. data/lib/rubydb/rails/transaction.rb +105 -105
  332. data/lib/rubydb/rails/type.rb +126 -126
  333. data/lib/rubydb/recovery/checkpoint.rb +261 -257
  334. data/lib/rubydb/recovery/consistency.rb +457 -467
  335. data/lib/rubydb/recovery/corruption_detector.rb +5 -5
  336. data/lib/rubydb/recovery/crash_recovery.rb +381 -387
  337. data/lib/rubydb/recovery/recovery_manager.rb +204 -206
  338. data/lib/rubydb/recovery/redo.rb +235 -237
  339. data/lib/rubydb/recovery/undo.rb +204 -206
  340. data/lib/rubydb/replication/failover.rb +5 -5
  341. data/lib/rubydb/replication/fencing.rb +63 -63
  342. data/lib/rubydb/replication/primary.rb +461 -450
  343. data/lib/rubydb/replication/replica.rb +382 -384
  344. data/lib/rubydb/replication/replication_log.rb +194 -200
  345. data/lib/rubydb/replication/replication_manager.rb +307 -308
  346. data/lib/rubydb/replication/replication_slot.rb +293 -295
  347. data/lib/rubydb/replication/replication_stream.rb +198 -201
  348. data/lib/rubydb/rubydb.rb +570 -560
  349. data/lib/rubydb/security/access_control.rb +252 -254
  350. data/lib/rubydb/security/audit_log.rb +209 -213
  351. data/lib/rubydb/security/authentication.rb +302 -302
  352. data/lib/rubydb/security/authorization.rb +282 -282
  353. data/lib/rubydb/security/credentials.rb +192 -196
  354. data/lib/rubydb/security/password.rb +205 -215
  355. data/lib/rubydb/security/permissions.rb +74 -74
  356. data/lib/rubydb/security/role.rb +99 -101
  357. data/lib/rubydb/security/user.rb +86 -86
  358. data/lib/rubydb/server/connection.rb +383 -366
  359. data/lib/rubydb/server/connection_pool.rb +193 -193
  360. data/lib/rubydb/server/lifecycle.rb +227 -228
  361. data/lib/rubydb/server/listener.rb +139 -136
  362. data/lib/rubydb/server/request_handler.rb +277 -276
  363. data/lib/rubydb/server/server.rb +363 -364
  364. data/lib/rubydb/server/session.rb +416 -369
  365. data/lib/rubydb/server/worker.rb +206 -210
  366. data/lib/rubydb/server/worker_pool.rb +168 -168
  367. data/lib/rubydb/sql/ast/alter_table.rb +169 -169
  368. data/lib/rubydb/sql/ast/begin_transaction.rb +47 -47
  369. data/lib/rubydb/sql/ast/commit.rb +37 -37
  370. data/lib/rubydb/sql/ast/constraint.rb +92 -83
  371. data/lib/rubydb/sql/ast/create_database.rb +41 -41
  372. data/lib/rubydb/sql/ast/create_index.rb +61 -61
  373. data/lib/rubydb/sql/ast/create_schema.rb +52 -52
  374. data/lib/rubydb/sql/ast/create_table.rb +187 -187
  375. data/lib/rubydb/sql/ast/delete.rb +54 -54
  376. data/lib/rubydb/sql/ast/drop_database.rb +41 -41
  377. data/lib/rubydb/sql/ast/drop_index.rb +41 -41
  378. data/lib/rubydb/sql/ast/drop_schema.rb +49 -49
  379. data/lib/rubydb/sql/ast/drop_table.rb +49 -49
  380. data/lib/rubydb/sql/ast/explain.rb +64 -64
  381. data/lib/rubydb/sql/ast/expression.rb +617 -604
  382. data/lib/rubydb/sql/ast/insert.rb +66 -66
  383. data/lib/rubydb/sql/ast/node.rb +42 -42
  384. data/lib/rubydb/sql/ast/rollback.rb +63 -63
  385. data/lib/rubydb/sql/ast/savepoint.rb +59 -59
  386. data/lib/rubydb/sql/ast/select.rb +88 -88
  387. data/lib/rubydb/sql/ast/set_operation.rb +22 -20
  388. data/lib/rubydb/sql/ast/trigger.rb +35 -29
  389. data/lib/rubydb/sql/ast/update.rb +88 -88
  390. data/lib/rubydb/sql/ast/vacuum.rb +19 -19
  391. data/lib/rubydb/sql/ast/view.rb +38 -32
  392. data/lib/rubydb/sql/ast/with.rb +32 -32
  393. data/lib/rubydb/sql/grammar.rb +86 -86
  394. data/lib/rubydb/sql/keywords.rb +156 -156
  395. data/lib/rubydb/sql/lexer.rb +209 -214
  396. data/lib/rubydb/sql/operators.rb +100 -100
  397. data/lib/rubydb/sql/parser.rb +1167 -1170
  398. data/lib/rubydb/sql/planner/analyzer.rb +283 -302
  399. data/lib/rubydb/sql/planner/binder.rb +537 -550
  400. data/lib/rubydb/sql/planner/type_checker.rb +427 -431
  401. data/lib/rubydb/sql/token.rb +210 -210
  402. data/lib/rubydb/storage/buffer_frame.rb +44 -44
  403. data/lib/rubydb/storage/buffer_pool.rb +155 -155
  404. data/lib/rubydb/storage/database_lock.rb +74 -74
  405. data/lib/rubydb/storage/deserializer.rb +332 -342
  406. data/lib/rubydb/storage/engine.rb +2409 -2330
  407. data/lib/rubydb/storage/file_manager.rb +191 -187
  408. data/lib/rubydb/storage/free_space_map.rb +79 -81
  409. data/lib/rubydb/storage/page.rb +92 -94
  410. data/lib/rubydb/storage/page_allocator.rb +852 -855
  411. data/lib/rubydb/storage/page_header.rb +63 -67
  412. data/lib/rubydb/storage/page_manager.rb +127 -131
  413. data/lib/rubydb/storage/record.rb +58 -58
  414. data/lib/rubydb/storage/row.rb +78 -78
  415. data/lib/rubydb/storage/serializer.rb +51 -51
  416. data/lib/rubydb/storage/snapshot_reader.rb +167 -0
  417. data/lib/rubydb/storage/storage_layout.rb +151 -151
  418. data/lib/rubydb/storage/storage_manager.rb +114 -114
  419. data/lib/rubydb/storage/tuple.rb +458 -461
  420. data/lib/rubydb/storage/visibility_map.rb +964 -973
  421. data/lib/rubydb/transactions/commit_manager.rb +219 -220
  422. data/lib/rubydb/transactions/isolation.rb +98 -98
  423. data/lib/rubydb/transactions/lock.rb +76 -76
  424. data/lib/rubydb/transactions/lock_manager.rb +359 -362
  425. data/lib/rubydb/transactions/savepoint.rb +142 -143
  426. data/lib/rubydb/transactions/transaction.rb +214 -215
  427. data/lib/rubydb/transactions/transaction_id.rb +84 -84
  428. data/lib/rubydb/transactions/transaction_log.rb +256 -257
  429. data/lib/rubydb/transactions/transaction_manager.rb +434 -435
  430. data/lib/rubydb/types/bigint.rb +36 -36
  431. data/lib/rubydb/types/blob.rb +37 -37
  432. data/lib/rubydb/types/boolean.rb +34 -34
  433. data/lib/rubydb/types/date.rb +39 -39
  434. data/lib/rubydb/types/decimal.rb +48 -48
  435. data/lib/rubydb/types/float.rb +34 -34
  436. data/lib/rubydb/types/integer.rb +36 -36
  437. data/lib/rubydb/types/json.rb +41 -41
  438. data/lib/rubydb/types/null.rb +34 -34
  439. data/lib/rubydb/types/smallint.rb +36 -36
  440. data/lib/rubydb/types/text.rb +37 -37
  441. data/lib/rubydb/types/time.rb +46 -46
  442. data/lib/rubydb/types/timestamp.rb +39 -39
  443. data/lib/rubydb/types/type.rb +119 -119
  444. data/lib/rubydb/types/uuid.rb +47 -47
  445. data/lib/rubydb/types/varchar.rb +37 -37
  446. data/lib/rubydb/version.rb +32 -32
  447. data/lib/rubydb/wal/archive.rb +207 -193
  448. data/lib/rubydb/wal/checkpoint.rb +181 -183
  449. data/lib/rubydb/wal/lsn.rb +94 -94
  450. data/lib/rubydb/wal/reader.rb +259 -260
  451. data/lib/rubydb/wal/record.rb +105 -105
  452. data/lib/rubydb/wal/segment.rb +193 -193
  453. data/lib/rubydb/wal/wal.rb +481 -452
  454. data/lib/rubydb/wal/writer.rb +236 -236
  455. data/lib/rubydb.rb +7 -7
  456. data/packaging/docker/docker-compose.failover.yml +43 -43
  457. data/packaging/homebrew/rubydb.rb +19 -19
  458. data/rubydb.gemspec +70 -57
  459. data/scripts/benchmark +7 -7
  460. data/scripts/build_accelerator +49 -0
  461. data/scripts/durability_drill +37 -37
  462. data/scripts/fuzz +63 -63
  463. data/scripts/release +77 -42
  464. data/scripts/release_check +43 -43
  465. data/scripts/replication_failover_drill +268 -250
  466. data/scripts/replication_network_failover_drill +287 -255
  467. data/scripts/restore_drill +45 -45
  468. data/scripts/security +45 -0
  469. metadata +102 -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