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.
- checksums.yaml +4 -4
- data/.github/PULL_REQUEST_TEMPLATE.md +15 -15
- data/.github/workflows/benchmark.yml +26 -26
- data/.github/workflows/compatibility.yml +63 -63
- data/.github/workflows/fuzz.yml +33 -33
- data/.github/workflows/lint.yml +21 -21
- data/.github/workflows/operations.yml +24 -24
- data/.github/workflows/production-validation.yml +111 -111
- data/.github/workflows/release.yml +77 -77
- data/.github/workflows/security.yml +39 -37
- data/.github/workflows/test.yml +26 -26
- data/.github/workflows/workload.yml +58 -58
- data/.gitignore +16 -5
- data/.rubocop.yml +50 -44
- data/.standard.yml +9 -14
- data/ARCHITECTURE.md +21 -21
- data/CHANGELOG.md +57 -27
- data/CODE_OF_CONDUCT.md +13 -13
- data/CONTRIBUTING.md +29 -29
- data/GOVERNANCE.md +16 -16
- data/Gemfile +18 -17
- data/Gemfile.lock +125 -71
- data/README.md +168 -12
- data/ROADMAP.md +27 -27
- data/Rakefile +76 -71
- data/SECURITY.md +54 -54
- data/SUPPORT.md +14 -14
- data/accelerator/bin/SHA256SUMS +6 -0
- data/accelerator/bin/rubydb-accelerator-darwin-amd64 +0 -0
- data/accelerator/bin/rubydb-accelerator-darwin-arm64 +0 -0
- data/accelerator/bin/rubydb-accelerator-linux-amd64 +0 -0
- data/accelerator/bin/rubydb-accelerator-linux-arm64 +0 -0
- data/accelerator/bin/rubydb-accelerator-windows-amd64.exe +0 -0
- data/accelerator/bin/rubydb-accelerator-windows-arm64.exe +0 -0
- data/accelerator/cmd/rubydb-accelerator/main.go +11 -0
- data/accelerator/go.mod +3 -0
- data/accelerator/internal/execution/aggregate.go +94 -0
- data/accelerator/internal/execution/distinct.go +22 -0
- data/accelerator/internal/execution/filter.go +73 -0
- data/accelerator/internal/execution/join.go +79 -0
- data/accelerator/internal/execution/operators.go +167 -0
- data/accelerator/internal/execution/scan.go +20 -0
- data/accelerator/internal/execution/sort.go +62 -0
- data/accelerator/internal/execution/types.go +136 -0
- data/accelerator/internal/execution/value.go +67 -0
- data/accelerator/internal/memory/arena.go +47 -0
- data/accelerator/internal/memory/reuse.go +22 -0
- data/accelerator/internal/metrics/registry.go +67 -0
- data/accelerator/internal/parallel/bounded_queue.go +56 -0
- data/accelerator/internal/parallel/scheduler.go +47 -0
- data/accelerator/internal/parallel/worker_pool.go +53 -0
- data/accelerator/internal/protocol/cancellation.go +48 -0
- data/accelerator/internal/protocol/columnar.go +263 -0
- data/accelerator/internal/protocol/frame.go +187 -0
- data/accelerator/internal/runtime/worker.go +521 -0
- data/accelerator/internal/storage/page_reader.go +81 -0
- data/accelerator/internal/storage/snapshot_scan.go +539 -0
- data/accelerator/internal/wal/checksum.go +13 -0
- data/accelerator/internal/wal/compression.go +41 -0
- data/accelerator/internal/wal/group_commit.go +24 -0
- data/accelerator/internal/wal/record_encoder.go +40 -0
- data/adapters/activerecord/Gemfile +11 -11
- data/adapters/activerecord/README.md +8 -3
- data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +881 -879
- data/adapters/activerecord/rubydb-activerecord.gemspec +21 -21
- data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +143 -143
- data/adapters/ruby/README.md +18 -18
- data/adapters/sequel/README.md +11 -11
- data/config/monitoring/prometheus-alerts.yml +39 -39
- data/config/production.yml +36 -36
- data/docs/README.md +77 -71
- data/docs/architecture/concurrency.md +14 -14
- data/docs/architecture/current-state.md +125 -125
- data/docs/architecture/execution-engine.md +25 -25
- data/docs/architecture/go-accelerator.md +179 -0
- data/docs/architecture/indexes.md +19 -19
- data/docs/architecture/mvcc.md +19 -19
- data/docs/architecture/overview.md +13 -13
- data/docs/architecture/pages.md +11 -11
- data/docs/architecture/production-roadmap.md +82 -82
- data/docs/architecture/query-planner.md +20 -20
- data/docs/architecture/recovery.md +18 -18
- data/docs/architecture/sql-engine.md +12 -12
- data/docs/architecture/storage-engine.md +14 -14
- data/docs/architecture/transactions.md +10 -10
- data/docs/architecture/wal.md +28 -28
- data/docs/cli-cheatsheet.md +98 -98
- data/docs/cli.md +299 -275
- data/docs/contributing/architecture.md +9 -9
- data/docs/contributing/benchmarking.md +30 -14
- data/docs/contributing/development.md +16 -16
- data/docs/contributing/release-process.md +49 -49
- data/docs/contributing/testing.md +16 -16
- data/docs/debugging.md +229 -229
- data/docs/developer/branching.md +10 -10
- data/docs/developer/database-diff.md +10 -10
- data/docs/developer/local-development.md +49 -17
- data/docs/developer/snapshots.md +9 -9
- data/docs/developer/temporal-data.md +10 -10
- data/docs/developer-guide.md +297 -297
- data/docs/getting-started/first-database.md +16 -16
- data/docs/getting-started/first-query.md +13 -13
- data/docs/getting-started/installation.md +19 -19
- data/docs/getting-started/local-to-production.md +300 -300
- data/docs/getting-started/quickstart.md +17 -17
- data/docs/getting-started/rails.md +16 -16
- data/docs/hardening_backlog.md +93 -93
- data/docs/lessons-learned.md +112 -112
- data/docs/operations/backups.md +33 -33
- data/docs/operations/disaster-recovery.md +31 -31
- data/docs/operations/failover.md +30 -30
- data/docs/operations/monitoring.md +25 -25
- data/docs/operations/production-guide.md +295 -295
- data/docs/operations/production-runbook.md +45 -45
- data/docs/operations/replication.md +33 -33
- data/docs/operations/restore.md +6 -6
- data/docs/operations/runbook.md +34 -34
- data/docs/operations/upgrades.md +14 -14
- data/docs/operations/workload-testing.md +17 -17
- data/docs/production-readiness.md +118 -118
- data/docs/production_validation.md +150 -150
- data/docs/rails/active-record.md +11 -11
- data/docs/rails/compatibility-guide.md +90 -90
- data/docs/rails/database-yml.md +92 -92
- data/docs/rails/installation.md +17 -17
- data/docs/rails/migrations.md +17 -17
- data/docs/rails/production.md +82 -82
- data/docs/rails/troubleshooting.md +18 -18
- data/docs/release.md +25 -25
- data/docs/server/architecture.md +10 -10
- data/docs/server/authentication.md +10 -10
- data/docs/server/configuration.md +16 -16
- data/docs/server/connection-pooling.md +10 -10
- data/docs/server/deployment.md +10 -10
- data/docs/server/protocol.md +12 -12
- data/docs/sql/compatibility-guide.md +82 -82
- data/docs/sql/compatibility.md +39 -39
- data/docs/sql/data-types.md +10 -10
- data/docs/sql/functions.md +9 -9
- data/docs/sql/joins.md +9 -9
- data/docs/sql/operators.md +9 -9
- data/docs/sql/sqlite-compatibility.md +21 -21
- data/docs/sql/syntax.md +10 -10
- data/docs/sql/transactions.md +10 -10
- data/docs/troubleshooting.md +244 -244
- data/lessons/01-foundations.md +73 -0
- data/lessons/02-local-development.md +121 -0
- data/lessons/03-embedded-rubydb.md +99 -0
- data/lessons/04-rails-complex-apps.md +138 -0
- data/lessons/05-rubydb-production-server.md +237 -0
- data/lessons/06-postgresql-massive-apps.md +96 -0
- data/lessons/07-hybrid-microservices.md +179 -0
- data/lessons/08-migrations-backups-recovery.md +86 -0
- data/lessons/09-observability-security-scale.md +87 -0
- data/lessons/10-release-readiness.md +192 -0
- data/lessons/11-community-adapter.md +323 -0
- data/lessons/12-rails-ecommerce-pressure.md +263 -0
- data/lib/rubydb/accelerator/client.rb +451 -0
- data/lib/rubydb/accelerator/error.rb +22 -0
- data/lib/rubydb/accelerator/manager.rb +606 -0
- data/lib/rubydb/accelerator.rb +13 -0
- data/lib/rubydb/backup/archive.rb +332 -334
- data/lib/rubydb/backup/backup.rb +400 -401
- data/lib/rubydb/backup/incremental.rb +349 -353
- data/lib/rubydb/backup/restore.rb +289 -290
- data/lib/rubydb/backup/snapshot.rb +265 -267
- data/lib/rubydb/backup/verification.rb +276 -279
- data/lib/rubydb/branching/branch.rb +181 -181
- data/lib/rubydb/branching/branch_manager.rb +307 -311
- data/lib/rubydb/branching/branch_metadata.rb +140 -140
- data/lib/rubydb/branching/checkout.rb +165 -166
- data/lib/rubydb/branching/copy_on_write.rb +272 -272
- data/lib/rubydb/branching/diff.rb +137 -138
- data/lib/rubydb/branching/merge.rb +282 -285
- data/lib/rubydb/build_info.rb +15 -15
- data/lib/rubydb/catalog/catalog.rb +391 -391
- data/lib/rubydb/catalog/column.rb +112 -112
- data/lib/rubydb/catalog/constraint.rb +180 -180
- data/lib/rubydb/catalog/database.rb +184 -184
- data/lib/rubydb/catalog/index.rb +97 -97
- data/lib/rubydb/catalog/schema.rb +103 -103
- data/lib/rubydb/catalog/sequence.rb +90 -90
- data/lib/rubydb/catalog/system_catalog.rb +698 -698
- data/lib/rubydb/catalog/table.rb +178 -178
- data/lib/rubydb/catalog/trigger.rb +102 -102
- data/lib/rubydb/catalog/view.rb +66 -66
- data/lib/rubydb/cli/application.rb +168 -163
- data/lib/rubydb/cli/commands/accelerator.rb +72 -0
- data/lib/rubydb/cli/commands/backup.rb +80 -81
- data/lib/rubydb/cli/commands/branch.rb +72 -72
- data/lib/rubydb/cli/commands/checkout.rb +54 -54
- data/lib/rubydb/cli/commands/create.rb +58 -58
- data/lib/rubydb/cli/commands/diff.rb +76 -77
- data/lib/rubydb/cli/commands/doctor.rb +77 -74
- data/lib/rubydb/cli/commands/drop.rb +57 -57
- data/lib/rubydb/cli/commands/init.rb +101 -102
- data/lib/rubydb/cli/commands/inspect.rb +95 -95
- data/lib/rubydb/cli/commands/merge.rb +63 -63
- data/lib/rubydb/cli/commands/migrate.rb +62 -62
- data/lib/rubydb/cli/commands/restart.rb +42 -39
- data/lib/rubydb/cli/commands/restore.rb +121 -121
- data/lib/rubydb/cli/commands/shell.rb +365 -365
- data/lib/rubydb/cli/commands/snapshot.rb +79 -79
- data/lib/rubydb/cli/commands/start.rb +88 -82
- data/lib/rubydb/cli/commands/status.rb +96 -92
- data/lib/rubydb/cli/commands/stop.rb +47 -47
- data/lib/rubydb/cli/commands/vacuum.rb +58 -58
- data/lib/rubydb/cli/formatter.rb +221 -221
- data/lib/rubydb/cli/output.rb +168 -168
- data/lib/rubydb/client/client.rb +309 -304
- data/lib/rubydb/client/connection.rb +429 -415
- data/lib/rubydb/client/connection_pool.rb +168 -168
- data/lib/rubydb/client/connection_url.rb +96 -96
- data/lib/rubydb/client/prepared_statement.rb +60 -60
- data/lib/rubydb/client/result.rb +127 -123
- data/lib/rubydb/client/statement.rb +52 -52
- data/lib/rubydb/client/transaction.rb +130 -130
- data/lib/rubydb/concurrency/concurrency.rb +19 -19
- data/lib/rubydb/concurrency/deadlock_detector.rb +148 -150
- data/lib/rubydb/concurrency/latch.rb +101 -101
- data/lib/rubydb/concurrency/lock_graph.rb +163 -165
- data/lib/rubydb/concurrency/mutex.rb +181 -183
- data/lib/rubydb/concurrency/rw_lock.rb +180 -180
- data/lib/rubydb/concurrency/scheduler.rb +248 -250
- data/lib/rubydb/concurrency/worker_pool.rb +145 -143
- data/lib/rubydb/configuration/config.rb +170 -170
- data/lib/rubydb/configuration/defaults.rb +191 -179
- data/lib/rubydb/configuration/environment.rb +152 -152
- data/lib/rubydb/configuration/parser.rb +185 -185
- data/lib/rubydb/configuration/validation.rb +228 -221
- data/lib/rubydb/constants.rb +74 -74
- data/lib/rubydb/constraints/check.rb +181 -181
- data/lib/rubydb/constraints/constraint.rb +101 -101
- data/lib/rubydb/constraints/foreign_key.rb +130 -130
- data/lib/rubydb/constraints/not_null.rb +64 -64
- data/lib/rubydb/constraints/primary_key.rb +99 -99
- data/lib/rubydb/constraints/unique.rb +106 -108
- data/lib/rubydb/constraints/validator.rb +349 -350
- data/lib/rubydb/errors/authentication_error.rb +10 -10
- data/lib/rubydb/errors/authorization_error.rb +23 -23
- data/lib/rubydb/errors/client_error.rb +10 -10
- data/lib/rubydb/errors/configuration_error.rb +10 -10
- data/lib/rubydb/errors/connection_error.rb +10 -10
- data/lib/rubydb/errors/constraint_error.rb +23 -23
- data/lib/rubydb/errors/corruption_error.rb +10 -10
- data/lib/rubydb/errors/database_error.rb +10 -10
- data/lib/rubydb/errors/error.rb +20 -20
- data/lib/rubydb/errors/execution_error.rb +10 -10
- data/lib/rubydb/errors/parser_error.rb +10 -10
- data/lib/rubydb/errors/recovery_error.rb +10 -10
- data/lib/rubydb/errors/replication_error.rb +10 -10
- data/lib/rubydb/errors/server_error.rb +6 -6
- data/lib/rubydb/errors/storage_error.rb +10 -10
- data/lib/rubydb/errors/transaction_error.rb +10 -10
- data/lib/rubydb/execution/accelerator_dispatch.rb +30 -0
- data/lib/rubydb/execution/aggregate_executor.rb +134 -138
- data/lib/rubydb/execution/cost_model.rb +72 -0
- data/lib/rubydb/execution/delete_executor.rb +110 -112
- data/lib/rubydb/execution/distinct_executor.rb +131 -135
- data/lib/rubydb/execution/executor.rb +1544 -1188
- data/lib/rubydb/execution/expression.rb +191 -193
- data/lib/rubydb/execution/index_scan.rb +142 -142
- data/lib/rubydb/execution/insert_executor.rb +215 -217
- data/lib/rubydb/execution/join_executor.rb +243 -249
- data/lib/rubydb/execution/limit_executor.rb +83 -85
- data/lib/rubydb/execution/operator_selection.rb +57 -0
- data/lib/rubydb/execution/optimizer.rb +227 -215
- data/lib/rubydb/execution/physical_plan.rb +47 -0
- data/lib/rubydb/execution/plan.rb +355 -353
- data/lib/rubydb/execution/planner.rb +508 -536
- data/lib/rubydb/execution/predicate.rb +235 -235
- data/lib/rubydb/execution/scan.rb +49 -49
- data/lib/rubydb/execution/sequential_scan.rb +63 -63
- data/lib/rubydb/execution/sort_executor.rb +194 -185
- data/lib/rubydb/execution/update_executor.rb +160 -162
- data/lib/rubydb/functions/aggregate.rb +70 -70
- data/lib/rubydb/functions/date_functions.rb +274 -278
- data/lib/rubydb/functions/function.rb +85 -85
- data/lib/rubydb/functions/json_functions.rb +231 -215
- data/lib/rubydb/functions/numeric_functions.rb +346 -346
- data/lib/rubydb/functions/scalar.rb +52 -52
- data/lib/rubydb/functions/string_functions.rb +383 -383
- data/lib/rubydb/functions/system_functions.rb +258 -246
- data/lib/rubydb/history/as_of.rb +238 -238
- data/lib/rubydb/history/change.rb +105 -105
- data/lib/rubydb/history/history.rb +131 -131
- data/lib/rubydb/history/history_manager.rb +228 -229
- data/lib/rubydb/history/temporal_query.rb +202 -202
- data/lib/rubydb/history/timeline.rb +144 -144
- data/lib/rubydb/indexes/btree.rb +215 -186
- data/lib/rubydb/indexes/btree_cursor.rb +258 -258
- data/lib/rubydb/indexes/btree_node.rb +384 -385
- data/lib/rubydb/indexes/hash_index.rb +150 -150
- data/lib/rubydb/indexes/index.rb +71 -71
- data/lib/rubydb/indexes/index_manager.rb +408 -406
- data/lib/rubydb/indexes/index_scan.rb +466 -470
- data/lib/rubydb/migrations/migration.rb +253 -254
- data/lib/rubydb/migrations/migration_lock.rb +146 -146
- data/lib/rubydb/migrations/migration_manager.rb +187 -176
- data/lib/rubydb/migrations/migration_version.rb +71 -71
- data/lib/rubydb/migrations/schema_diff.rb +211 -211
- data/lib/rubydb/migrations/schema_version.rb +64 -64
- data/lib/rubydb/monitoring/events.rb +155 -160
- data/lib/rubydb/monitoring/health.rb +216 -222
- data/lib/rubydb/monitoring/logger.rb +188 -193
- data/lib/rubydb/monitoring/metrics.rb +363 -359
- data/lib/rubydb/monitoring/performance.rb +176 -176
- data/lib/rubydb/monitoring/statistics.rb +168 -170
- data/lib/rubydb/mvcc/garbage_collector.rb +199 -199
- data/lib/rubydb/mvcc/mvcc.rb +16 -16
- data/lib/rubydb/mvcc/snapshot.rb +146 -147
- data/lib/rubydb/mvcc/vacuum.rb +180 -180
- data/lib/rubydb/mvcc/version.rb +106 -106
- data/lib/rubydb/mvcc/version_store.rb +396 -398
- data/lib/rubydb/mvcc/visibility.rb +107 -109
- data/lib/rubydb/protocol/capabilities.rb +125 -125
- data/lib/rubydb/protocol/decoder.rb +142 -145
- data/lib/rubydb/protocol/encoder.rb +131 -136
- data/lib/rubydb/protocol/handshake.rb +306 -305
- data/lib/rubydb/protocol/message.rb +121 -121
- data/lib/rubydb/protocol/parameter_binder.rb +101 -0
- data/lib/rubydb/protocol/protocol.rb +276 -277
- data/lib/rubydb/protocol/version.rb +54 -54
- data/lib/rubydb/rails/adapter.rb +245 -239
- data/lib/rubydb/rails/connection.rb +312 -314
- data/lib/rubydb/rails/database_statements.rb +122 -122
- data/lib/rubydb/rails/migration.rb +131 -131
- data/lib/rubydb/rails/quoting.rb +109 -109
- data/lib/rubydb/rails/result.rb +117 -117
- data/lib/rubydb/rails/schema_statements.rb +339 -339
- data/lib/rubydb/rails/transaction.rb +105 -105
- data/lib/rubydb/rails/type.rb +126 -126
- data/lib/rubydb/recovery/checkpoint.rb +261 -257
- data/lib/rubydb/recovery/consistency.rb +457 -467
- data/lib/rubydb/recovery/corruption_detector.rb +5 -5
- data/lib/rubydb/recovery/crash_recovery.rb +381 -387
- data/lib/rubydb/recovery/recovery_manager.rb +204 -206
- data/lib/rubydb/recovery/redo.rb +235 -237
- data/lib/rubydb/recovery/undo.rb +204 -206
- data/lib/rubydb/replication/failover.rb +5 -5
- data/lib/rubydb/replication/fencing.rb +63 -63
- data/lib/rubydb/replication/primary.rb +461 -450
- data/lib/rubydb/replication/replica.rb +382 -384
- data/lib/rubydb/replication/replication_log.rb +194 -200
- data/lib/rubydb/replication/replication_manager.rb +307 -308
- data/lib/rubydb/replication/replication_slot.rb +293 -295
- data/lib/rubydb/replication/replication_stream.rb +198 -201
- data/lib/rubydb/rubydb.rb +570 -560
- data/lib/rubydb/security/access_control.rb +252 -254
- data/lib/rubydb/security/audit_log.rb +209 -213
- data/lib/rubydb/security/authentication.rb +302 -302
- data/lib/rubydb/security/authorization.rb +282 -282
- data/lib/rubydb/security/credentials.rb +192 -196
- data/lib/rubydb/security/password.rb +205 -215
- data/lib/rubydb/security/permissions.rb +74 -74
- data/lib/rubydb/security/role.rb +99 -101
- data/lib/rubydb/security/user.rb +86 -86
- data/lib/rubydb/server/connection.rb +383 -366
- data/lib/rubydb/server/connection_pool.rb +193 -193
- data/lib/rubydb/server/lifecycle.rb +227 -228
- data/lib/rubydb/server/listener.rb +139 -136
- data/lib/rubydb/server/request_handler.rb +277 -276
- data/lib/rubydb/server/server.rb +363 -364
- data/lib/rubydb/server/session.rb +416 -369
- data/lib/rubydb/server/worker.rb +206 -210
- data/lib/rubydb/server/worker_pool.rb +168 -168
- data/lib/rubydb/sql/ast/alter_table.rb +169 -169
- data/lib/rubydb/sql/ast/begin_transaction.rb +47 -47
- data/lib/rubydb/sql/ast/commit.rb +37 -37
- data/lib/rubydb/sql/ast/constraint.rb +92 -83
- data/lib/rubydb/sql/ast/create_database.rb +41 -41
- data/lib/rubydb/sql/ast/create_index.rb +61 -61
- data/lib/rubydb/sql/ast/create_schema.rb +52 -52
- data/lib/rubydb/sql/ast/create_table.rb +187 -187
- data/lib/rubydb/sql/ast/delete.rb +54 -54
- data/lib/rubydb/sql/ast/drop_database.rb +41 -41
- data/lib/rubydb/sql/ast/drop_index.rb +41 -41
- data/lib/rubydb/sql/ast/drop_schema.rb +49 -49
- data/lib/rubydb/sql/ast/drop_table.rb +49 -49
- data/lib/rubydb/sql/ast/explain.rb +64 -64
- data/lib/rubydb/sql/ast/expression.rb +617 -604
- data/lib/rubydb/sql/ast/insert.rb +66 -66
- data/lib/rubydb/sql/ast/node.rb +42 -42
- data/lib/rubydb/sql/ast/rollback.rb +63 -63
- data/lib/rubydb/sql/ast/savepoint.rb +59 -59
- data/lib/rubydb/sql/ast/select.rb +88 -88
- data/lib/rubydb/sql/ast/set_operation.rb +22 -20
- data/lib/rubydb/sql/ast/trigger.rb +35 -29
- data/lib/rubydb/sql/ast/update.rb +88 -88
- data/lib/rubydb/sql/ast/vacuum.rb +19 -19
- data/lib/rubydb/sql/ast/view.rb +38 -32
- data/lib/rubydb/sql/ast/with.rb +32 -32
- data/lib/rubydb/sql/grammar.rb +86 -86
- data/lib/rubydb/sql/keywords.rb +156 -156
- data/lib/rubydb/sql/lexer.rb +209 -214
- data/lib/rubydb/sql/operators.rb +100 -100
- data/lib/rubydb/sql/parser.rb +1167 -1170
- data/lib/rubydb/sql/planner/analyzer.rb +283 -302
- data/lib/rubydb/sql/planner/binder.rb +537 -550
- data/lib/rubydb/sql/planner/type_checker.rb +427 -431
- data/lib/rubydb/sql/token.rb +210 -210
- data/lib/rubydb/storage/buffer_frame.rb +44 -44
- data/lib/rubydb/storage/buffer_pool.rb +155 -155
- data/lib/rubydb/storage/database_lock.rb +74 -74
- data/lib/rubydb/storage/deserializer.rb +332 -342
- data/lib/rubydb/storage/engine.rb +2409 -2330
- data/lib/rubydb/storage/file_manager.rb +191 -187
- data/lib/rubydb/storage/free_space_map.rb +79 -81
- data/lib/rubydb/storage/page.rb +92 -94
- data/lib/rubydb/storage/page_allocator.rb +852 -855
- data/lib/rubydb/storage/page_header.rb +63 -67
- data/lib/rubydb/storage/page_manager.rb +127 -131
- data/lib/rubydb/storage/record.rb +58 -58
- data/lib/rubydb/storage/row.rb +78 -78
- data/lib/rubydb/storage/serializer.rb +51 -51
- data/lib/rubydb/storage/snapshot_reader.rb +167 -0
- data/lib/rubydb/storage/storage_layout.rb +151 -151
- data/lib/rubydb/storage/storage_manager.rb +114 -114
- data/lib/rubydb/storage/tuple.rb +458 -461
- data/lib/rubydb/storage/visibility_map.rb +964 -973
- data/lib/rubydb/transactions/commit_manager.rb +219 -220
- data/lib/rubydb/transactions/isolation.rb +98 -98
- data/lib/rubydb/transactions/lock.rb +76 -76
- data/lib/rubydb/transactions/lock_manager.rb +359 -362
- data/lib/rubydb/transactions/savepoint.rb +142 -143
- data/lib/rubydb/transactions/transaction.rb +214 -215
- data/lib/rubydb/transactions/transaction_id.rb +84 -84
- data/lib/rubydb/transactions/transaction_log.rb +256 -257
- data/lib/rubydb/transactions/transaction_manager.rb +434 -435
- data/lib/rubydb/types/bigint.rb +36 -36
- data/lib/rubydb/types/blob.rb +37 -37
- data/lib/rubydb/types/boolean.rb +34 -34
- data/lib/rubydb/types/date.rb +39 -39
- data/lib/rubydb/types/decimal.rb +48 -48
- data/lib/rubydb/types/float.rb +34 -34
- data/lib/rubydb/types/integer.rb +36 -36
- data/lib/rubydb/types/json.rb +41 -41
- data/lib/rubydb/types/null.rb +34 -34
- data/lib/rubydb/types/smallint.rb +36 -36
- data/lib/rubydb/types/text.rb +37 -37
- data/lib/rubydb/types/time.rb +46 -46
- data/lib/rubydb/types/timestamp.rb +39 -39
- data/lib/rubydb/types/type.rb +119 -119
- data/lib/rubydb/types/uuid.rb +47 -47
- data/lib/rubydb/types/varchar.rb +37 -37
- data/lib/rubydb/version.rb +32 -32
- data/lib/rubydb/wal/archive.rb +207 -193
- data/lib/rubydb/wal/checkpoint.rb +181 -183
- data/lib/rubydb/wal/lsn.rb +94 -94
- data/lib/rubydb/wal/reader.rb +259 -260
- data/lib/rubydb/wal/record.rb +105 -105
- data/lib/rubydb/wal/segment.rb +193 -193
- data/lib/rubydb/wal/wal.rb +481 -452
- data/lib/rubydb/wal/writer.rb +236 -236
- data/lib/rubydb.rb +7 -7
- data/packaging/docker/docker-compose.failover.yml +43 -43
- data/packaging/homebrew/rubydb.rb +19 -19
- data/rubydb.gemspec +70 -57
- data/scripts/benchmark +7 -7
- data/scripts/build_accelerator +49 -0
- data/scripts/durability_drill +37 -37
- data/scripts/fuzz +63 -63
- data/scripts/release +77 -42
- data/scripts/release_check +43 -43
- data/scripts/replication_failover_drill +268 -250
- data/scripts/replication_network_failover_drill +287 -255
- data/scripts/restore_drill +45 -45
- data/scripts/security +45 -0
- metadata +102 -1
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "json"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "rbconfig"
|
|
8
|
+
require "securerandom"
|
|
9
|
+
require "timeout"
|
|
10
|
+
|
|
11
|
+
module RubyDB
|
|
12
|
+
module Accelerator
|
|
13
|
+
# Owns one long-lived Go accelerator process. It deliberately uses a
|
|
14
|
+
# private stdio channel instead of a public TCP listener: there is no port
|
|
15
|
+
# to expose, and the Ruby process remains the database owner.
|
|
16
|
+
class Manager
|
|
17
|
+
DEFAULT_TIMEOUT = 30
|
|
18
|
+
DEFAULT_MAX_FRAME_SIZE = 16 * 1024 * 1024
|
|
19
|
+
PROTOCOL_VERSION = 1
|
|
20
|
+
BINARY_VERSION = 1
|
|
21
|
+
BINARY_MAGIC = "RDBB".b
|
|
22
|
+
BINARY_ROWS_TYPE = 1
|
|
23
|
+
BINARY_JOIN_TYPE = 2
|
|
24
|
+
BINARY_SNAPSHOT_SCAN_TYPE = 3
|
|
25
|
+
|
|
26
|
+
attr_reader :mode, :binary_path, :min_rows, :read_pipeline, :capabilities, :max_rows, :max_result_bytes
|
|
27
|
+
|
|
28
|
+
def initialize(config = {})
|
|
29
|
+
config = config.transform_keys(&:to_sym)
|
|
30
|
+
@mode = (config[:mode] || ENV.fetch("RUBYDB_ACCELERATOR", "auto")).to_s.downcase
|
|
31
|
+
validate_mode!
|
|
32
|
+
@binary_path = resolve_binary(config[:binary] || ENV["RUBYDB_ACCELERATOR_BIN"])
|
|
33
|
+
@timeout = Float(config[:timeout] || ENV.fetch("RUBYDB_ACCELERATOR_TIMEOUT", DEFAULT_TIMEOUT))
|
|
34
|
+
@max_frame_size = Integer(config[:max_frame_size] || DEFAULT_MAX_FRAME_SIZE)
|
|
35
|
+
@max_rows = Integer(config[:max_rows] || ENV.fetch("RUBYDB_ACCELERATOR_MAX_ROWS", "1000000"))
|
|
36
|
+
@max_result_bytes = Integer(config[:max_result_bytes] || ENV.fetch("RUBYDB_ACCELERATOR_MAX_RESULT_BYTES", @max_frame_size))
|
|
37
|
+
@min_rows = Integer(config[:min_rows] || ENV.fetch("RUBYDB_ACCELERATOR_MIN_ROWS", 256))
|
|
38
|
+
@read_pipeline = (config[:read_pipeline] || ENV.fetch("RUBYDB_ACCELERATOR_READ_PIPELINE", "on")).to_s.downcase
|
|
39
|
+
raise ArgumentError, "read_pipeline must be off or on" unless %w[off on].include?(@read_pipeline)
|
|
40
|
+
raise ArgumentError, "min_rows must be non-negative" if @min_rows.negative?
|
|
41
|
+
raise ArgumentError, "timeout must be positive" unless @timeout.positive?
|
|
42
|
+
raise ArgumentError, "max_frame_size must be positive" unless @max_frame_size.positive?
|
|
43
|
+
raise ArgumentError, "max_rows must be positive" unless @max_rows.positive?
|
|
44
|
+
raise ArgumentError, "max_result_bytes must be positive" unless @max_result_bytes.positive?
|
|
45
|
+
@mutex = Mutex.new
|
|
46
|
+
@write_mutex = Mutex.new
|
|
47
|
+
@handshake_mutex = Mutex.new
|
|
48
|
+
@pending = {}
|
|
49
|
+
@stdin = nil
|
|
50
|
+
@stdout = nil
|
|
51
|
+
@stderr = nil
|
|
52
|
+
@wait_thread = nil
|
|
53
|
+
@reader_thread = nil
|
|
54
|
+
@stderr_thread = nil
|
|
55
|
+
@started = false
|
|
56
|
+
@handshaken = false
|
|
57
|
+
@stopping = false
|
|
58
|
+
@worker_started_once = false
|
|
59
|
+
@worker_starts = 0
|
|
60
|
+
@worker_restarts = 0
|
|
61
|
+
@worker_failures = 0
|
|
62
|
+
@capabilities = []
|
|
63
|
+
@performance_decisions = {}
|
|
64
|
+
@performance_samples = Hash.new { |hash, key| hash[key] = {count: 0, fallbacks: 0, total_ms: 0.0, rows: 0} }
|
|
65
|
+
@last_error = nil
|
|
66
|
+
rescue ArgumentError, TypeError => error
|
|
67
|
+
raise Error, "Invalid RubyDB accelerator configuration: #{error.message}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def available?
|
|
71
|
+
return false if @mode == "off"
|
|
72
|
+
|
|
73
|
+
!@binary_path.nil? && File.file?(@binary_path)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def enabled?
|
|
77
|
+
@mode != "off" && available?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def started?
|
|
81
|
+
@mutex.synchronize { @started }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def stats
|
|
85
|
+
@mutex.synchronize do
|
|
86
|
+
{
|
|
87
|
+
mode: @mode,
|
|
88
|
+
available: available?,
|
|
89
|
+
started: @started,
|
|
90
|
+
binary_path: @binary_path,
|
|
91
|
+
protocol_version: PROTOCOL_VERSION,
|
|
92
|
+
capabilities: @capabilities.dup,
|
|
93
|
+
performance_decisions: @performance_decisions.dup,
|
|
94
|
+
performance_samples: @performance_samples.transform_values(&:dup),
|
|
95
|
+
min_rows: @min_rows,
|
|
96
|
+
max_rows: @max_rows,
|
|
97
|
+
max_result_bytes: @max_result_bytes,
|
|
98
|
+
read_pipeline: @read_pipeline,
|
|
99
|
+
worker_pid: worker_pid,
|
|
100
|
+
worker_starts: @worker_starts,
|
|
101
|
+
worker_restarts: @worker_restarts,
|
|
102
|
+
worker_failures: @worker_failures,
|
|
103
|
+
last_error: @last_error
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Restart the private worker after an external failure. The next request
|
|
109
|
+
# also starts a failed worker lazily, but this explicit operation is
|
|
110
|
+
# useful to supervisors and release/soak tests that need to verify the
|
|
111
|
+
# lifecycle independently of a query.
|
|
112
|
+
def restart
|
|
113
|
+
@mutex.synchronize do
|
|
114
|
+
stop_locked
|
|
115
|
+
@stopping = false
|
|
116
|
+
start_locked
|
|
117
|
+
end
|
|
118
|
+
true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def request(type, payload = {}, timeout: @timeout)
|
|
122
|
+
return nil if @mode == "off"
|
|
123
|
+
return nil unless prepare_transport!
|
|
124
|
+
ensure_handshake(timeout)
|
|
125
|
+
exchange_json(type.to_s, payload, timeout)
|
|
126
|
+
rescue Error
|
|
127
|
+
raise
|
|
128
|
+
rescue => error
|
|
129
|
+
@last_error = "#{error.class}: #{error.message}"
|
|
130
|
+
@mutex.synchronize { stop_locked }
|
|
131
|
+
raise ProtocolError, "RubyDB accelerator communication failed: #{error.message}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Sends a bounded binary request over the same private stdio channel.
|
|
135
|
+
# Large row batches avoid JSON and base64 conversion on both sides.
|
|
136
|
+
def binary_request(type, payload, timeout: @timeout)
|
|
137
|
+
return nil if @mode == "off"
|
|
138
|
+
return nil unless prepare_transport!
|
|
139
|
+
ensure_handshake(timeout)
|
|
140
|
+
exchange_binary(type.to_s, payload, timeout)
|
|
141
|
+
rescue Error
|
|
142
|
+
raise
|
|
143
|
+
rescue => error
|
|
144
|
+
@last_error = "#{error.class}: #{error.message}"
|
|
145
|
+
@mutex.synchronize { stop_locked }
|
|
146
|
+
raise ProtocolError, "RubyDB accelerator communication failed: #{error.message}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Collects a sequence of bounded JSON result batches from the worker.
|
|
150
|
+
# The worker may execute the request concurrently with other requests;
|
|
151
|
+
# only this request's queue is consumed here.
|
|
152
|
+
def request_stream(type, payload = {}, timeout: @timeout)
|
|
153
|
+
return nil if @mode == "off"
|
|
154
|
+
return nil unless prepare_transport!
|
|
155
|
+
ensure_handshake(timeout)
|
|
156
|
+
|
|
157
|
+
id = "acc_#{SecureRandom.hex(12)}"
|
|
158
|
+
queue = register_pending(id)
|
|
159
|
+
write_locked(message(type.to_s, payload, id))
|
|
160
|
+
rows = []
|
|
161
|
+
aggregates = nil
|
|
162
|
+
Timeout.timeout(timeout, TimeoutError) do
|
|
163
|
+
loop do
|
|
164
|
+
response = queue.pop
|
|
165
|
+
raise response if response.is_a?(Exception)
|
|
166
|
+
unless response["success"] == true
|
|
167
|
+
raise RequestError.new(response["error"].to_s, code: response["code"])
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
batch = response["payload"] || {}
|
|
171
|
+
rows.concat(Array(batch["rows"]))
|
|
172
|
+
aggregates = batch["aggregates"] if batch.key?("aggregates")
|
|
173
|
+
break unless response["more"] == true
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
{"rows" => rows, "row_count" => rows.length, "aggregates" => aggregates}
|
|
177
|
+
rescue TimeoutError
|
|
178
|
+
cancel_request(id, timeout)
|
|
179
|
+
raise TimeoutError.new("RubyDB accelerator request timed out", code: "timeout")
|
|
180
|
+
ensure
|
|
181
|
+
@mutex.synchronize { @pending.delete(id) } if id
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def supports?(capability)
|
|
185
|
+
@mutex.synchronize { @capabilities.include?(capability.to_s) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def performance_calibrated?(workload)
|
|
189
|
+
@mutex.synchronize { @performance_decisions.key?(workload.to_sym) }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def acceleration_preferred?(workload)
|
|
193
|
+
return true if @mode == "required"
|
|
194
|
+
|
|
195
|
+
@mutex.synchronize { @performance_decisions.fetch(workload.to_sym, true) }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def record_performance(workload, ruby_ms:, go_ms:)
|
|
199
|
+
return true if @mode == "required"
|
|
200
|
+
|
|
201
|
+
preferred = go_ms < ruby_ms
|
|
202
|
+
@mutex.synchronize { @performance_decisions[workload.to_sym] = preferred }
|
|
203
|
+
preferred
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def observe_performance(workload, milliseconds:, rows: 0, fallback: false)
|
|
207
|
+
@mutex.synchronize do
|
|
208
|
+
sample = @performance_samples[workload.to_sym]
|
|
209
|
+
sample[:count] += 1
|
|
210
|
+
sample[:fallbacks] += 1 if fallback
|
|
211
|
+
sample[:total_ms] += Float(milliseconds)
|
|
212
|
+
sample[:rows] += Integer(rows)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def accelerator_policy(workload, input_rows:, estimated_bytes: 0, deadline_at: nil)
|
|
217
|
+
return false if @mode == "off"
|
|
218
|
+
return false unless supports?(workload_capability(workload))
|
|
219
|
+
return false if input_rows && input_rows.to_i > @max_rows
|
|
220
|
+
return false if estimated_bytes.to_i > @max_result_bytes
|
|
221
|
+
return false if deadline_at && Time.now >= deadline_at
|
|
222
|
+
|
|
223
|
+
acceleration_preferred?(workload)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def close
|
|
227
|
+
@mutex.synchronize do
|
|
228
|
+
if @started && @stdin && !@stdin.closed?
|
|
229
|
+
begin
|
|
230
|
+
write_locked(message("terminate"))
|
|
231
|
+
rescue
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
stop_locked
|
|
236
|
+
end
|
|
237
|
+
true
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
def prepare_transport!
|
|
243
|
+
return nil if @mode == "off"
|
|
244
|
+
|
|
245
|
+
@mutex.synchronize do
|
|
246
|
+
unless available?
|
|
247
|
+
raise UnavailableError.new("RubyDB accelerator binary is not available", code: "binary_missing") if @mode == "required"
|
|
248
|
+
|
|
249
|
+
return nil
|
|
250
|
+
end
|
|
251
|
+
start_locked
|
|
252
|
+
true
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def workload_capability(workload)
|
|
257
|
+
case workload.to_sym
|
|
258
|
+
when :scan, :index_scan, :sort, :aggregate, :distinct, :window, :join, :merge_join
|
|
259
|
+
"rows_pipeline"
|
|
260
|
+
when :snapshot_scan
|
|
261
|
+
"snapshot_scan"
|
|
262
|
+
when :compression
|
|
263
|
+
"gzip"
|
|
264
|
+
when :checksum
|
|
265
|
+
"sha256"
|
|
266
|
+
when :wal_batch
|
|
267
|
+
"wal_batch"
|
|
268
|
+
else
|
|
269
|
+
workload.to_s
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def validate_mode!
|
|
274
|
+
return if %w[auto off required].include?(@mode)
|
|
275
|
+
|
|
276
|
+
raise ArgumentError, "mode must be auto, off, or required"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def start_locked
|
|
280
|
+
return if @started
|
|
281
|
+
if @binary_path.nil? || !File.file?(@binary_path)
|
|
282
|
+
raise UnavailableError.new("RubyDB accelerator binary is not available", code: "binary_missing")
|
|
283
|
+
end
|
|
284
|
+
verify_binary_integrity!
|
|
285
|
+
|
|
286
|
+
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(@binary_path)
|
|
287
|
+
# The worker protocol carries both newline-delimited JSON and binary
|
|
288
|
+
# frames. Ruby's Windows pipes default to text/UTF-8 behavior, which
|
|
289
|
+
# rejects binary bytes such as a snapshot page header. Put every pipe
|
|
290
|
+
# in binary mode before the reader or request threads can use it.
|
|
291
|
+
@stdin.binmode
|
|
292
|
+
@stdout.binmode
|
|
293
|
+
@stderr.binmode
|
|
294
|
+
@started = true
|
|
295
|
+
@handshaken = false
|
|
296
|
+
@stopping = false
|
|
297
|
+
@worker_starts += 1
|
|
298
|
+
@worker_restarts += 1 if @worker_started_once
|
|
299
|
+
@worker_started_once = true
|
|
300
|
+
stdout = @stdout
|
|
301
|
+
@reader_thread = Thread.new { reader_loop(stdout) }
|
|
302
|
+
stderr = @stderr
|
|
303
|
+
@stderr_thread = Thread.new do
|
|
304
|
+
stderr.each_line do |line|
|
|
305
|
+
@last_error = line.to_s.strip unless line.to_s.strip.empty?
|
|
306
|
+
end
|
|
307
|
+
rescue IOError
|
|
308
|
+
nil
|
|
309
|
+
end
|
|
310
|
+
rescue Errno::ENOENT => error
|
|
311
|
+
@last_error = error.message
|
|
312
|
+
stop_locked
|
|
313
|
+
raise UnavailableError.new("Unable to start RubyDB accelerator: #{error.message}", code: "spawn_failed")
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def ensure_handshake(timeout)
|
|
317
|
+
@handshake_mutex.synchronize do
|
|
318
|
+
return if @mutex.synchronize { @handshaken }
|
|
319
|
+
|
|
320
|
+
response = exchange_json("handshake", {
|
|
321
|
+
protocol_version: PROTOCOL_VERSION,
|
|
322
|
+
client_name: "rubydb-ruby",
|
|
323
|
+
client_version: (defined?(RubyDB::VERSION) ? RubyDB::VERSION : "unknown")
|
|
324
|
+
}, timeout)
|
|
325
|
+
unless response["protocol_version"].to_i == PROTOCOL_VERSION
|
|
326
|
+
@mutex.synchronize { stop_locked }
|
|
327
|
+
raise ProtocolError.new("RubyDB accelerator protocol handshake failed", code: "protocol_mismatch")
|
|
328
|
+
end
|
|
329
|
+
@mutex.synchronize do
|
|
330
|
+
@capabilities = Array(response["capabilities"]).map(&:to_s)
|
|
331
|
+
@handshaken = true
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def exchange_json(type, payload, timeout)
|
|
337
|
+
id = "acc_#{SecureRandom.hex(12)}"
|
|
338
|
+
queue = register_pending(id)
|
|
339
|
+
write_locked(message(type, payload, id))
|
|
340
|
+
response = wait_for_pending(id, queue, timeout)
|
|
341
|
+
unless response["success"] == true
|
|
342
|
+
raise RequestError.new(response["error"].to_s, code: response["code"])
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
response["payload"] || {}
|
|
346
|
+
rescue TimeoutError
|
|
347
|
+
cancel_request(id, timeout)
|
|
348
|
+
raise TimeoutError.new("RubyDB accelerator request timed out", code: "timeout")
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def register_pending(id)
|
|
352
|
+
queue = Queue.new
|
|
353
|
+
@mutex.synchronize do
|
|
354
|
+
raise ProtocolError.new("RubyDB accelerator is not running", code: "not_started") unless @started
|
|
355
|
+
|
|
356
|
+
@pending[id] = queue
|
|
357
|
+
end
|
|
358
|
+
queue
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def wait_for_pending(id, queue, timeout)
|
|
362
|
+
value = Timeout.timeout(timeout, TimeoutError) { queue.pop }
|
|
363
|
+
raise value if value.is_a?(Exception)
|
|
364
|
+
|
|
365
|
+
value
|
|
366
|
+
ensure
|
|
367
|
+
@mutex.synchronize { @pending.delete(id) }
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def cancel_request(target_id, timeout)
|
|
371
|
+
return unless @mutex.synchronize { @started }
|
|
372
|
+
|
|
373
|
+
cancel_id = "acc_cancel_#{SecureRandom.hex(8)}"
|
|
374
|
+
queue = register_pending(cancel_id)
|
|
375
|
+
write_locked(message("cancel", {target_id: target_id}, cancel_id))
|
|
376
|
+
wait_for_pending(cancel_id, queue, [Float(timeout), 1.0].min)
|
|
377
|
+
rescue Timeout::Error, TimeoutError, IOError, SystemCallError, ProtocolError
|
|
378
|
+
@mutex.synchronize { stop_locked }
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def write_locked(value)
|
|
382
|
+
frame = "#{JSON.generate(value)}\n"
|
|
383
|
+
raise ProtocolError.new("RubyDB accelerator request exceeds max frame size", code: "frame_too_large") if frame.bytesize > @max_frame_size
|
|
384
|
+
|
|
385
|
+
@write_mutex.synchronize do
|
|
386
|
+
@stdin.write(frame)
|
|
387
|
+
@stdin.flush
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def exchange_binary(type, payload, timeout)
|
|
392
|
+
type_id = case type
|
|
393
|
+
when "rows_pipeline", "rows_pipeline_binary" then BINARY_ROWS_TYPE
|
|
394
|
+
when "hash_join" then BINARY_JOIN_TYPE
|
|
395
|
+
when "snapshot_scan" then BINARY_SNAPSHOT_SCAN_TYPE
|
|
396
|
+
else
|
|
397
|
+
raise RequestError.new("Unsupported binary accelerator operation", code: "unsupported_operation")
|
|
398
|
+
end
|
|
399
|
+
id = "acc_#{SecureRandom.hex(12)}"
|
|
400
|
+
queue = register_pending(id)
|
|
401
|
+
write_binary_locked(type_id, id, payload)
|
|
402
|
+
result = wait_for_pending(id, queue, timeout)
|
|
403
|
+
if result.is_a?(Hash) && result[:binary_error]
|
|
404
|
+
error_payload = JSON.parse(result[:binary_error])
|
|
405
|
+
raise RequestError.new(error_payload["error"].to_s, code: error_payload["code"])
|
|
406
|
+
end
|
|
407
|
+
result
|
|
408
|
+
rescue TimeoutError
|
|
409
|
+
cancel_request(id, timeout)
|
|
410
|
+
raise TimeoutError.new("RubyDB accelerator request timed out", code: "timeout")
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def write_binary_locked(type_id, id, payload)
|
|
414
|
+
payload = payload.to_s.b
|
|
415
|
+
if id.bytesize > 1024 || payload.bytesize > @max_frame_size || id.bytesize + payload.bytesize > @max_frame_size
|
|
416
|
+
raise ProtocolError.new("RubyDB accelerator binary request exceeds max frame size", code: "frame_too_large")
|
|
417
|
+
end
|
|
418
|
+
header = [BINARY_MAGIC, BINARY_VERSION, 1, type_id, 0, id.bytesize, payload.bytesize].pack("a4C4vV")
|
|
419
|
+
@write_mutex.synchronize do
|
|
420
|
+
@stdin.write(header)
|
|
421
|
+
@stdin.write(id)
|
|
422
|
+
@stdin.write(payload)
|
|
423
|
+
@stdin.flush
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def reader_loop(stdout)
|
|
428
|
+
loop do
|
|
429
|
+
prefix = stdout.read(4)
|
|
430
|
+
raise EOFError if prefix.nil? || prefix.empty? || prefix.bytesize != 4
|
|
431
|
+
|
|
432
|
+
if prefix == BINARY_MAGIC
|
|
433
|
+
header = prefix + read_from_worker(stdout, 10)
|
|
434
|
+
raise ProtocolError.new("Invalid binary response header", code: "invalid_response") unless header.getbyte(4) == BINARY_VERSION
|
|
435
|
+
|
|
436
|
+
id_length = header.byteslice(8, 2).unpack1("v")
|
|
437
|
+
payload_length = header.byteslice(10, 4).unpack1("V")
|
|
438
|
+
raise ProtocolError.new("Binary response exceeds max frame size", code: "frame_too_large") if id_length > 1024 || payload_length > @max_frame_size || id_length + payload_length > @max_frame_size
|
|
439
|
+
|
|
440
|
+
body = read_from_worker(stdout, id_length + payload_length)
|
|
441
|
+
id = body.byteslice(0, id_length).to_s
|
|
442
|
+
payload = body.byteslice(id_length, payload_length).to_s.b
|
|
443
|
+
value = if header.getbyte(7) == 0
|
|
444
|
+
payload
|
|
445
|
+
else
|
|
446
|
+
{binary_error: payload}
|
|
447
|
+
end
|
|
448
|
+
deliver_pending(id, value)
|
|
449
|
+
else
|
|
450
|
+
line = prefix + stdout.gets.to_s
|
|
451
|
+
raise ProtocolError.new("Invalid JSON response", code: "invalid_response") if line.bytesize > @max_frame_size
|
|
452
|
+
|
|
453
|
+
response = JSON.parse(line)
|
|
454
|
+
deliver_pending(response["id"].to_s, response)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
rescue => error
|
|
458
|
+
mark_worker_failure(error)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def mark_worker_failure(error)
|
|
462
|
+
message = "#{error.class}: #{error.message}"
|
|
463
|
+
streams = nil
|
|
464
|
+
pending = []
|
|
465
|
+
@mutex.synchronize do
|
|
466
|
+
return if @stopping
|
|
467
|
+
|
|
468
|
+
@last_error = message
|
|
469
|
+
@worker_failures += 1
|
|
470
|
+
@started = false
|
|
471
|
+
@handshaken = false
|
|
472
|
+
pending = @pending.values.dup
|
|
473
|
+
@pending.clear
|
|
474
|
+
streams = [@stdin, @stdout, @stderr]
|
|
475
|
+
@stdin = @stdout = @stderr = nil
|
|
476
|
+
end
|
|
477
|
+
pending.each { |queue| queue << ProtocolError.new("RubyDB accelerator reader stopped: #{message}", code: "reader_stopped") }
|
|
478
|
+
streams.each do |stream|
|
|
479
|
+
stream.close unless stream.nil? || stream.closed?
|
|
480
|
+
rescue IOError
|
|
481
|
+
nil
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def read_from_worker(stdout, length)
|
|
486
|
+
value = stdout.read(length)
|
|
487
|
+
raise EOFError if value.nil? || value.bytesize != length
|
|
488
|
+
|
|
489
|
+
value
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def deliver_pending(id, value)
|
|
493
|
+
queue = @mutex.synchronize { @pending[id] }
|
|
494
|
+
queue << value if queue
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def message(type, payload = {}, id = "acc_#{SecureRandom.hex(12)}")
|
|
498
|
+
{"id" => id, "type" => type, "payload" => payload}
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def stop_locked
|
|
502
|
+
@stopping = true
|
|
503
|
+
stdin = @stdin
|
|
504
|
+
stdout = @stdout
|
|
505
|
+
stderr = @stderr
|
|
506
|
+
wait_thread = @wait_thread
|
|
507
|
+
reader_thread = @reader_thread
|
|
508
|
+
@stdin = @stdout = @stderr = @wait_thread = nil
|
|
509
|
+
@reader_thread = nil
|
|
510
|
+
@started = false
|
|
511
|
+
@handshaken = false
|
|
512
|
+
@capabilities = []
|
|
513
|
+
|
|
514
|
+
pending = @pending.values.dup
|
|
515
|
+
@pending.clear
|
|
516
|
+
pending.each { |queue| queue << ProtocolError.new("RubyDB accelerator stopped", code: "stopped") }
|
|
517
|
+
|
|
518
|
+
[stdin, stdout, stderr].each do |io|
|
|
519
|
+
io&.close unless io.nil? || io.closed?
|
|
520
|
+
rescue IOError
|
|
521
|
+
nil
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
if wait_thread
|
|
525
|
+
begin
|
|
526
|
+
wait_thread.join(0.25)
|
|
527
|
+
if wait_thread.alive?
|
|
528
|
+
pid = wait_thread.respond_to?(:pid) ? wait_thread.pid : nil
|
|
529
|
+
Process.kill("TERM", pid) if pid
|
|
530
|
+
wait_thread.join(0.25)
|
|
531
|
+
end
|
|
532
|
+
rescue SystemCallError
|
|
533
|
+
nil
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
if reader_thread && reader_thread != Thread.current
|
|
538
|
+
reader_thread.kill
|
|
539
|
+
reader_thread.join(0.25)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
@stderr_thread&.kill
|
|
543
|
+
@stderr_thread = nil
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def worker_pid
|
|
547
|
+
return nil unless @wait_thread&.respond_to?(:pid)
|
|
548
|
+
|
|
549
|
+
@wait_thread.pid
|
|
550
|
+
rescue
|
|
551
|
+
nil
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def resolve_binary(configured)
|
|
555
|
+
return File.expand_path(configured.to_s) unless configured.nil? || configured.to_s.empty?
|
|
556
|
+
|
|
557
|
+
filename = "rubydb-accelerator-#{platform_name}"
|
|
558
|
+
filename += ".exe" if windows?
|
|
559
|
+
candidates = [
|
|
560
|
+
File.join(__dir__, "bin", filename),
|
|
561
|
+
File.expand_path(File.join(__dir__, "..", "..", "..", "accelerator", "bin", filename)),
|
|
562
|
+
File.expand_path(File.join(Dir.pwd, "accelerator", "bin", filename))
|
|
563
|
+
]
|
|
564
|
+
candidates.find { |path| File.file?(path) }
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def verify_binary_integrity!
|
|
568
|
+
manifest = File.join(File.dirname(@binary_path), "SHA256SUMS")
|
|
569
|
+
return unless File.file?(manifest)
|
|
570
|
+
|
|
571
|
+
entry = File.readlines(manifest, chomp: true).find do |line|
|
|
572
|
+
digest, name = line.split(/\s+/, 2)
|
|
573
|
+
name == File.basename(@binary_path) && digest && digest.match?(/\A[0-9a-f]{64}\z/i)
|
|
574
|
+
end
|
|
575
|
+
raise UnavailableError.new("RubyDB accelerator binary is not listed in SHA256SUMS", code: "binary_checksum_missing") unless entry
|
|
576
|
+
|
|
577
|
+
expected = entry.split(/\s+/, 2).first.downcase
|
|
578
|
+
actual = Digest::SHA256.file(@binary_path).hexdigest
|
|
579
|
+
return if expected == actual
|
|
580
|
+
|
|
581
|
+
raise UnavailableError.new("RubyDB accelerator binary checksum mismatch", code: "binary_checksum_mismatch")
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def windows?
|
|
585
|
+
RbConfig::CONFIG["host_os"].to_s.match?(/mswin|mingw|cygwin/i)
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def platform_name
|
|
589
|
+
os = if windows?
|
|
590
|
+
"windows"
|
|
591
|
+
elsif RbConfig::CONFIG["host_os"].to_s.match?(/darwin/i)
|
|
592
|
+
"darwin"
|
|
593
|
+
else
|
|
594
|
+
"linux"
|
|
595
|
+
end
|
|
596
|
+
cpu = RbConfig::CONFIG["host_cpu"].to_s
|
|
597
|
+
arch = case cpu
|
|
598
|
+
when /aarch64|arm64/ then "arm64"
|
|
599
|
+
when /386|i.86/ then "386"
|
|
600
|
+
else "amd64"
|
|
601
|
+
end
|
|
602
|
+
"#{os}-#{arch}"
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "accelerator/error"
|
|
4
|
+
require_relative "accelerator/manager"
|
|
5
|
+
require_relative "accelerator/client"
|
|
6
|
+
|
|
7
|
+
module RubyDB
|
|
8
|
+
# Optional performance accelerator. The Ruby implementation remains the
|
|
9
|
+
# source of truth; this client only delegates explicitly supported, pure
|
|
10
|
+
# read or utility operations to a managed Go worker.
|
|
11
|
+
module Accelerator
|
|
12
|
+
end
|
|
13
|
+
end
|