rubydb 0.1.0
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 +7 -0
- data/.gitattributes +20 -0
- data/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- data/.github/ISSUE_TEMPLATE/feature.yml +0 -0
- data/.github/ISSUE_TEMPLATE/performance.yml +0 -0
- data/.github/ISSUE_TEMPLATE/security.yml +0 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +15 -0
- data/.github/dependabot.yml +0 -0
- data/.github/workflows/benchmark.yml +26 -0
- data/.github/workflows/compatibility.yml +63 -0
- data/.github/workflows/fuzz.yml +33 -0
- data/.github/workflows/lint.yml +21 -0
- data/.github/workflows/operations.yml +24 -0
- data/.github/workflows/production-validation.yml +111 -0
- data/.github/workflows/release.yml +77 -0
- data/.github/workflows/security.yml +37 -0
- data/.github/workflows/test.yml +26 -0
- data/.github/workflows/workload.yml +58 -0
- data/.gitignore +43 -0
- data/.rspec +4 -0
- data/.rubocop.yml +44 -0
- data/.ruby-version +1 -0
- data/.standard.yml +14 -0
- data/ARCHITECTURE.md +21 -0
- data/CHANGELOG.md +31 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +29 -0
- data/GOVERNANCE.md +16 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +71 -0
- data/LICENSE +60 -0
- data/README.md +162 -0
- data/ROADMAP.md +27 -0
- data/Rakefile +71 -0
- data/SECURITY.md +54 -0
- data/SUPPORT.md +14 -0
- data/adapters/activerecord/Gemfile +11 -0
- data/adapters/activerecord/README.md +9 -0
- data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +887 -0
- data/adapters/activerecord/rubydb-activerecord.gemspec +27 -0
- data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +151 -0
- data/adapters/ruby/README.md +18 -0
- data/adapters/sequel/README.md +11 -0
- data/bin/rubydb +19 -0
- data/bin/rubydb-server +126 -0
- data/config/development.yml +0 -0
- data/config/monitoring/prometheus-alerts.yml +39 -0
- data/config/production.yml +36 -0
- data/config/test.yml +0 -0
- data/docs/README.md +71 -0
- data/docs/architecture/concurrency.md +14 -0
- data/docs/architecture/current-state.md +125 -0
- data/docs/architecture/execution-engine.md +25 -0
- data/docs/architecture/indexes.md +19 -0
- data/docs/architecture/mvcc.md +19 -0
- data/docs/architecture/overview.md +13 -0
- data/docs/architecture/pages.md +11 -0
- data/docs/architecture/production-roadmap.md +82 -0
- data/docs/architecture/query-planner.md +20 -0
- data/docs/architecture/recovery.md +18 -0
- data/docs/architecture/sql-engine.md +12 -0
- data/docs/architecture/storage-engine.md +14 -0
- data/docs/architecture/transactions.md +10 -0
- data/docs/architecture/wal.md +28 -0
- data/docs/cli-cheatsheet.md +98 -0
- data/docs/cli.md +275 -0
- data/docs/contributing/architecture.md +9 -0
- data/docs/contributing/benchmarking.md +14 -0
- data/docs/contributing/development.md +16 -0
- data/docs/contributing/release-process.md +49 -0
- data/docs/contributing/testing.md +16 -0
- data/docs/debugging.md +229 -0
- data/docs/developer/branching.md +10 -0
- data/docs/developer/database-diff.md +10 -0
- data/docs/developer/local-development.md +17 -0
- data/docs/developer/snapshots.md +9 -0
- data/docs/developer/temporal-data.md +10 -0
- data/docs/developer-guide.md +297 -0
- data/docs/getting-started/first-database.md +16 -0
- data/docs/getting-started/first-query.md +13 -0
- data/docs/getting-started/installation.md +19 -0
- data/docs/getting-started/local-to-production.md +300 -0
- data/docs/getting-started/quickstart.md +17 -0
- data/docs/getting-started/rails.md +16 -0
- data/docs/hardening_backlog.md +93 -0
- data/docs/lessons-learned.md +112 -0
- data/docs/operations/backups.md +33 -0
- data/docs/operations/disaster-recovery.md +31 -0
- data/docs/operations/failover.md +30 -0
- data/docs/operations/monitoring.md +25 -0
- data/docs/operations/production-guide.md +295 -0
- data/docs/operations/production-runbook.md +45 -0
- data/docs/operations/replication.md +33 -0
- data/docs/operations/restore.md +6 -0
- data/docs/operations/runbook.md +34 -0
- data/docs/operations/upgrades.md +14 -0
- data/docs/operations/workload-testing.md +17 -0
- data/docs/production-readiness.md +118 -0
- data/docs/production_validation.md +150 -0
- data/docs/rails/active-record.md +11 -0
- data/docs/rails/compatibility-guide.md +90 -0
- data/docs/rails/database-yml.md +92 -0
- data/docs/rails/installation.md +17 -0
- data/docs/rails/migrations.md +17 -0
- data/docs/rails/production.md +82 -0
- data/docs/rails/troubleshooting.md +18 -0
- data/docs/release.md +59 -0
- data/docs/server/architecture.md +10 -0
- data/docs/server/authentication.md +10 -0
- data/docs/server/configuration.md +16 -0
- data/docs/server/connection-pooling.md +10 -0
- data/docs/server/deployment.md +10 -0
- data/docs/server/protocol.md +12 -0
- data/docs/sql/compatibility-guide.md +82 -0
- data/docs/sql/compatibility.md +39 -0
- data/docs/sql/data-types.md +10 -0
- data/docs/sql/functions.md +9 -0
- data/docs/sql/joins.md +9 -0
- data/docs/sql/operators.md +9 -0
- data/docs/sql/sqlite-compatibility.md +21 -0
- data/docs/sql/syntax.md +10 -0
- data/docs/sql/transactions.md +10 -0
- data/docs/troubleshooting.md +244 -0
- data/exe/rubydb +23 -0
- data/lib/rubydb/backup/archive.rb +334 -0
- data/lib/rubydb/backup/backup.rb +401 -0
- data/lib/rubydb/backup/incremental.rb +353 -0
- data/lib/rubydb/backup/restore.rb +290 -0
- data/lib/rubydb/backup/snapshot.rb +267 -0
- data/lib/rubydb/backup/verification.rb +279 -0
- data/lib/rubydb/branching/branch.rb +181 -0
- data/lib/rubydb/branching/branch_manager.rb +311 -0
- data/lib/rubydb/branching/branch_metadata.rb +140 -0
- data/lib/rubydb/branching/checkout.rb +166 -0
- data/lib/rubydb/branching/copy_on_write.rb +272 -0
- data/lib/rubydb/branching/diff.rb +138 -0
- data/lib/rubydb/branching/merge.rb +285 -0
- data/lib/rubydb/build_info.rb +15 -0
- data/lib/rubydb/catalog/catalog.rb +391 -0
- data/lib/rubydb/catalog/column.rb +112 -0
- data/lib/rubydb/catalog/constraint.rb +180 -0
- data/lib/rubydb/catalog/database.rb +184 -0
- data/lib/rubydb/catalog/index.rb +97 -0
- data/lib/rubydb/catalog/schema.rb +103 -0
- data/lib/rubydb/catalog/sequence.rb +90 -0
- data/lib/rubydb/catalog/system_catalog.rb +698 -0
- data/lib/rubydb/catalog/table.rb +178 -0
- data/lib/rubydb/catalog/trigger.rb +102 -0
- data/lib/rubydb/catalog/view.rb +66 -0
- data/lib/rubydb/cli/application.rb +163 -0
- data/lib/rubydb/cli/commands/backup.rb +81 -0
- data/lib/rubydb/cli/commands/branch.rb +72 -0
- data/lib/rubydb/cli/commands/checkout.rb +54 -0
- data/lib/rubydb/cli/commands/create.rb +58 -0
- data/lib/rubydb/cli/commands/diff.rb +77 -0
- data/lib/rubydb/cli/commands/doctor.rb +74 -0
- data/lib/rubydb/cli/commands/drop.rb +57 -0
- data/lib/rubydb/cli/commands/init.rb +102 -0
- data/lib/rubydb/cli/commands/inspect.rb +95 -0
- data/lib/rubydb/cli/commands/merge.rb +63 -0
- data/lib/rubydb/cli/commands/migrate.rb +62 -0
- data/lib/rubydb/cli/commands/restart.rb +39 -0
- data/lib/rubydb/cli/commands/restore.rb +121 -0
- data/lib/rubydb/cli/commands/shell.rb +365 -0
- data/lib/rubydb/cli/commands/snapshot.rb +79 -0
- data/lib/rubydb/cli/commands/start.rb +82 -0
- data/lib/rubydb/cli/commands/status.rb +92 -0
- data/lib/rubydb/cli/commands/stop.rb +47 -0
- data/lib/rubydb/cli/commands/vacuum.rb +58 -0
- data/lib/rubydb/cli/formatter.rb +221 -0
- data/lib/rubydb/cli/output.rb +168 -0
- data/lib/rubydb/client/client.rb +304 -0
- data/lib/rubydb/client/connection.rb +415 -0
- data/lib/rubydb/client/connection_pool.rb +168 -0
- data/lib/rubydb/client/connection_url.rb +96 -0
- data/lib/rubydb/client/prepared_statement.rb +60 -0
- data/lib/rubydb/client/result.rb +123 -0
- data/lib/rubydb/client/statement.rb +52 -0
- data/lib/rubydb/client/transaction.rb +130 -0
- data/lib/rubydb/concurrency/concurrency.rb +19 -0
- data/lib/rubydb/concurrency/deadlock_detector.rb +150 -0
- data/lib/rubydb/concurrency/latch.rb +101 -0
- data/lib/rubydb/concurrency/lock_graph.rb +165 -0
- data/lib/rubydb/concurrency/mutex.rb +183 -0
- data/lib/rubydb/concurrency/rw_lock.rb +180 -0
- data/lib/rubydb/concurrency/scheduler.rb +250 -0
- data/lib/rubydb/concurrency/worker_pool.rb +143 -0
- data/lib/rubydb/configuration/config.rb +170 -0
- data/lib/rubydb/configuration/defaults.rb +179 -0
- data/lib/rubydb/configuration/environment.rb +152 -0
- data/lib/rubydb/configuration/parser.rb +185 -0
- data/lib/rubydb/configuration/validation.rb +221 -0
- data/lib/rubydb/constants.rb +74 -0
- data/lib/rubydb/constraints/check.rb +181 -0
- data/lib/rubydb/constraints/constraint.rb +101 -0
- data/lib/rubydb/constraints/foreign_key.rb +130 -0
- data/lib/rubydb/constraints/not_null.rb +64 -0
- data/lib/rubydb/constraints/primary_key.rb +99 -0
- data/lib/rubydb/constraints/unique.rb +108 -0
- data/lib/rubydb/constraints/validator.rb +350 -0
- data/lib/rubydb/errors/authentication_error.rb +10 -0
- data/lib/rubydb/errors/authorization_error.rb +23 -0
- data/lib/rubydb/errors/client_error.rb +10 -0
- data/lib/rubydb/errors/configuration_error.rb +10 -0
- data/lib/rubydb/errors/connection_error.rb +10 -0
- data/lib/rubydb/errors/constraint_error.rb +23 -0
- data/lib/rubydb/errors/corruption_error.rb +10 -0
- data/lib/rubydb/errors/database_error.rb +10 -0
- data/lib/rubydb/errors/error.rb +20 -0
- data/lib/rubydb/errors/execution_error.rb +10 -0
- data/lib/rubydb/errors/parser_error.rb +10 -0
- data/lib/rubydb/errors/recovery_error.rb +10 -0
- data/lib/rubydb/errors/replication_error.rb +10 -0
- data/lib/rubydb/errors/server_error.rb +6 -0
- data/lib/rubydb/errors/storage_error.rb +10 -0
- data/lib/rubydb/errors/transaction_error.rb +10 -0
- data/lib/rubydb/execution/aggregate_executor.rb +138 -0
- data/lib/rubydb/execution/delete_executor.rb +112 -0
- data/lib/rubydb/execution/distinct_executor.rb +135 -0
- data/lib/rubydb/execution/executor.rb +1192 -0
- data/lib/rubydb/execution/expression.rb +193 -0
- data/lib/rubydb/execution/index_scan.rb +142 -0
- data/lib/rubydb/execution/insert_executor.rb +217 -0
- data/lib/rubydb/execution/join_executor.rb +249 -0
- data/lib/rubydb/execution/limit_executor.rb +85 -0
- data/lib/rubydb/execution/optimizer.rb +215 -0
- data/lib/rubydb/execution/plan.rb +361 -0
- data/lib/rubydb/execution/planner.rb +546 -0
- data/lib/rubydb/execution/predicate.rb +235 -0
- data/lib/rubydb/execution/scan.rb +49 -0
- data/lib/rubydb/execution/sequential_scan.rb +63 -0
- data/lib/rubydb/execution/sort_executor.rb +185 -0
- data/lib/rubydb/execution/update_executor.rb +162 -0
- data/lib/rubydb/functions/aggregate.rb +70 -0
- data/lib/rubydb/functions/date_functions.rb +278 -0
- data/lib/rubydb/functions/function.rb +85 -0
- data/lib/rubydb/functions/json_functions.rb +215 -0
- data/lib/rubydb/functions/numeric_functions.rb +346 -0
- data/lib/rubydb/functions/scalar.rb +52 -0
- data/lib/rubydb/functions/string_functions.rb +383 -0
- data/lib/rubydb/functions/system_functions.rb +246 -0
- data/lib/rubydb/history/as_of.rb +238 -0
- data/lib/rubydb/history/change.rb +105 -0
- data/lib/rubydb/history/history.rb +131 -0
- data/lib/rubydb/history/history_manager.rb +229 -0
- data/lib/rubydb/history/temporal_query.rb +202 -0
- data/lib/rubydb/history/timeline.rb +144 -0
- data/lib/rubydb/indexes/btree.rb +186 -0
- data/lib/rubydb/indexes/btree_cursor.rb +258 -0
- data/lib/rubydb/indexes/btree_node.rb +385 -0
- data/lib/rubydb/indexes/hash_index.rb +150 -0
- data/lib/rubydb/indexes/index.rb +71 -0
- data/lib/rubydb/indexes/index_manager.rb +406 -0
- data/lib/rubydb/indexes/index_scan.rb +470 -0
- data/lib/rubydb/migrations/migration.rb +254 -0
- data/lib/rubydb/migrations/migration_lock.rb +146 -0
- data/lib/rubydb/migrations/migration_manager.rb +176 -0
- data/lib/rubydb/migrations/migration_version.rb +71 -0
- data/lib/rubydb/migrations/schema_diff.rb +211 -0
- data/lib/rubydb/migrations/schema_version.rb +64 -0
- data/lib/rubydb/monitoring/events.rb +160 -0
- data/lib/rubydb/monitoring/health.rb +222 -0
- data/lib/rubydb/monitoring/logger.rb +193 -0
- data/lib/rubydb/monitoring/metrics.rb +359 -0
- data/lib/rubydb/monitoring/performance.rb +176 -0
- data/lib/rubydb/monitoring/statistics.rb +170 -0
- data/lib/rubydb/mvcc/garbage_collector.rb +199 -0
- data/lib/rubydb/mvcc/mvcc.rb +16 -0
- data/lib/rubydb/mvcc/snapshot.rb +147 -0
- data/lib/rubydb/mvcc/vacuum.rb +180 -0
- data/lib/rubydb/mvcc/version.rb +106 -0
- data/lib/rubydb/mvcc/version_store.rb +398 -0
- data/lib/rubydb/mvcc/visibility.rb +109 -0
- data/lib/rubydb/protocol/capabilities.rb +125 -0
- data/lib/rubydb/protocol/decoder.rb +145 -0
- data/lib/rubydb/protocol/encoder.rb +136 -0
- data/lib/rubydb/protocol/handshake.rb +305 -0
- data/lib/rubydb/protocol/message.rb +121 -0
- data/lib/rubydb/protocol/protocol.rb +277 -0
- data/lib/rubydb/protocol/version.rb +54 -0
- data/lib/rubydb/rails/adapter.rb +239 -0
- data/lib/rubydb/rails/connection.rb +314 -0
- data/lib/rubydb/rails/database_statements.rb +122 -0
- data/lib/rubydb/rails/migration.rb +131 -0
- data/lib/rubydb/rails/quoting.rb +109 -0
- data/lib/rubydb/rails/result.rb +120 -0
- data/lib/rubydb/rails/schema_statements.rb +339 -0
- data/lib/rubydb/rails/transaction.rb +105 -0
- data/lib/rubydb/rails/type.rb +126 -0
- data/lib/rubydb/recovery/checkpoint.rb +257 -0
- data/lib/rubydb/recovery/consistency.rb +467 -0
- data/lib/rubydb/recovery/corruption_detector.rb +5 -0
- data/lib/rubydb/recovery/crash_recovery.rb +387 -0
- data/lib/rubydb/recovery/recovery_manager.rb +206 -0
- data/lib/rubydb/recovery/redo.rb +237 -0
- data/lib/rubydb/recovery/undo.rb +206 -0
- data/lib/rubydb/replication/failover.rb +5 -0
- data/lib/rubydb/replication/fencing.rb +63 -0
- data/lib/rubydb/replication/primary.rb +450 -0
- data/lib/rubydb/replication/replica.rb +384 -0
- data/lib/rubydb/replication/replication_log.rb +200 -0
- data/lib/rubydb/replication/replication_manager.rb +308 -0
- data/lib/rubydb/replication/replication_slot.rb +295 -0
- data/lib/rubydb/replication/replication_stream.rb +201 -0
- data/lib/rubydb/rubydb.rb +560 -0
- data/lib/rubydb/security/access_control.rb +254 -0
- data/lib/rubydb/security/audit_log.rb +213 -0
- data/lib/rubydb/security/authentication.rb +302 -0
- data/lib/rubydb/security/authorization.rb +282 -0
- data/lib/rubydb/security/credentials.rb +196 -0
- data/lib/rubydb/security/password.rb +215 -0
- data/lib/rubydb/security/permissions.rb +74 -0
- data/lib/rubydb/security/role.rb +101 -0
- data/lib/rubydb/security/user.rb +86 -0
- data/lib/rubydb/server/connection.rb +366 -0
- data/lib/rubydb/server/connection_pool.rb +193 -0
- data/lib/rubydb/server/lifecycle.rb +228 -0
- data/lib/rubydb/server/listener.rb +136 -0
- data/lib/rubydb/server/request_handler.rb +276 -0
- data/lib/rubydb/server/server.rb +364 -0
- data/lib/rubydb/server/session.rb +369 -0
- data/lib/rubydb/server/worker.rb +210 -0
- data/lib/rubydb/server/worker_pool.rb +168 -0
- data/lib/rubydb/sql/ast/alter_table.rb +169 -0
- data/lib/rubydb/sql/ast/begin_transaction.rb +47 -0
- data/lib/rubydb/sql/ast/commit.rb +37 -0
- data/lib/rubydb/sql/ast/constraint.rb +83 -0
- data/lib/rubydb/sql/ast/create_database.rb +41 -0
- data/lib/rubydb/sql/ast/create_index.rb +61 -0
- data/lib/rubydb/sql/ast/create_schema.rb +52 -0
- data/lib/rubydb/sql/ast/create_table.rb +187 -0
- data/lib/rubydb/sql/ast/delete.rb +54 -0
- data/lib/rubydb/sql/ast/drop_database.rb +41 -0
- data/lib/rubydb/sql/ast/drop_index.rb +41 -0
- data/lib/rubydb/sql/ast/drop_schema.rb +49 -0
- data/lib/rubydb/sql/ast/drop_table.rb +49 -0
- data/lib/rubydb/sql/ast/explain.rb +64 -0
- data/lib/rubydb/sql/ast/expression.rb +604 -0
- data/lib/rubydb/sql/ast/insert.rb +82 -0
- data/lib/rubydb/sql/ast/node.rb +42 -0
- data/lib/rubydb/sql/ast/rollback.rb +63 -0
- data/lib/rubydb/sql/ast/savepoint.rb +59 -0
- data/lib/rubydb/sql/ast/select.rb +88 -0
- data/lib/rubydb/sql/ast/set_operation.rb +20 -0
- data/lib/rubydb/sql/ast/trigger.rb +29 -0
- data/lib/rubydb/sql/ast/update.rb +88 -0
- data/lib/rubydb/sql/ast/vacuum.rb +19 -0
- data/lib/rubydb/sql/ast/view.rb +32 -0
- data/lib/rubydb/sql/ast/with.rb +32 -0
- data/lib/rubydb/sql/grammar.rb +86 -0
- data/lib/rubydb/sql/keywords.rb +156 -0
- data/lib/rubydb/sql/lexer.rb +214 -0
- data/lib/rubydb/sql/operators.rb +100 -0
- data/lib/rubydb/sql/parser.rb +1185 -0
- data/lib/rubydb/sql/planner/analyzer.rb +302 -0
- data/lib/rubydb/sql/planner/binder.rb +550 -0
- data/lib/rubydb/sql/planner/type_checker.rb +431 -0
- data/lib/rubydb/sql/token.rb +210 -0
- data/lib/rubydb/storage/buffer_frame.rb +44 -0
- data/lib/rubydb/storage/buffer_pool.rb +155 -0
- data/lib/rubydb/storage/database_lock.rb +74 -0
- data/lib/rubydb/storage/deserializer.rb +342 -0
- data/lib/rubydb/storage/engine.rb +2338 -0
- data/lib/rubydb/storage/file_manager.rb +187 -0
- data/lib/rubydb/storage/free_space_map.rb +81 -0
- data/lib/rubydb/storage/page.rb +94 -0
- data/lib/rubydb/storage/page_allocator.rb +855 -0
- data/lib/rubydb/storage/page_header.rb +67 -0
- data/lib/rubydb/storage/page_manager.rb +131 -0
- data/lib/rubydb/storage/record.rb +58 -0
- data/lib/rubydb/storage/row.rb +78 -0
- data/lib/rubydb/storage/serializer.rb +51 -0
- data/lib/rubydb/storage/storage_layout.rb +151 -0
- data/lib/rubydb/storage/storage_manager.rb +114 -0
- data/lib/rubydb/storage/tuple.rb +461 -0
- data/lib/rubydb/storage/visibility_map.rb +984 -0
- data/lib/rubydb/transactions/commit_manager.rb +220 -0
- data/lib/rubydb/transactions/isolation.rb +98 -0
- data/lib/rubydb/transactions/lock.rb +76 -0
- data/lib/rubydb/transactions/lock_manager.rb +362 -0
- data/lib/rubydb/transactions/savepoint.rb +143 -0
- data/lib/rubydb/transactions/transaction.rb +215 -0
- data/lib/rubydb/transactions/transaction_id.rb +84 -0
- data/lib/rubydb/transactions/transaction_log.rb +257 -0
- data/lib/rubydb/transactions/transaction_manager.rb +435 -0
- data/lib/rubydb/types/bigint.rb +36 -0
- data/lib/rubydb/types/blob.rb +37 -0
- data/lib/rubydb/types/boolean.rb +34 -0
- data/lib/rubydb/types/date.rb +39 -0
- data/lib/rubydb/types/decimal.rb +48 -0
- data/lib/rubydb/types/float.rb +34 -0
- data/lib/rubydb/types/integer.rb +36 -0
- data/lib/rubydb/types/json.rb +41 -0
- data/lib/rubydb/types/null.rb +34 -0
- data/lib/rubydb/types/smallint.rb +36 -0
- data/lib/rubydb/types/text.rb +37 -0
- data/lib/rubydb/types/time.rb +46 -0
- data/lib/rubydb/types/timestamp.rb +39 -0
- data/lib/rubydb/types/type.rb +119 -0
- data/lib/rubydb/types/uuid.rb +47 -0
- data/lib/rubydb/types/varchar.rb +37 -0
- data/lib/rubydb/version.rb +33 -0
- data/lib/rubydb/wal/archive.rb +193 -0
- data/lib/rubydb/wal/checkpoint.rb +183 -0
- data/lib/rubydb/wal/lsn.rb +94 -0
- data/lib/rubydb/wal/reader.rb +260 -0
- data/lib/rubydb/wal/record.rb +105 -0
- data/lib/rubydb/wal/segment.rb +193 -0
- data/lib/rubydb/wal/wal.rb +452 -0
- data/lib/rubydb/wal/writer.rb +236 -0
- data/lib/rubydb.rb +7 -0
- data/packaging/docker/Dockerfile +36 -0
- data/packaging/docker/docker-compose.failover.yml +43 -0
- data/packaging/docker/entrypoint.sh +15 -0
- data/packaging/homebrew/rubydb.rb +19 -0
- data/packaging/systemd/rubydb.service +22 -0
- data/rubydb.gemspec +57 -0
- data/scripts/benchmark +7 -0
- data/scripts/durability_drill +37 -0
- data/scripts/format +0 -0
- data/scripts/fuzz +63 -0
- data/scripts/lint +0 -0
- data/scripts/release +50 -0
- data/scripts/release_check +43 -0
- data/scripts/replication_failover_drill +250 -0
- data/scripts/replication_network_failover_drill +255 -0
- data/scripts/restore_drill +45 -0
- data/scripts/security +0 -0
- data/scripts/setup +0 -0
- data/scripts/test +0 -0
- data/scripts/test-chaos +0 -0
- data/scripts/test-crash +0 -0
- data/scripts/test-integration +0 -0
- data/scripts/test-unit +0 -0
- data/visibility_map.json +1 -0
- metadata +538 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module RubyDB
|
|
6
|
+
module Transactions
|
|
7
|
+
|
|
8
|
+
# Import lock and transaction classes
|
|
9
|
+
require_relative "lock"
|
|
10
|
+
require_relative "transaction"
|
|
11
|
+
|
|
12
|
+
# LockManager - Manages locks for transactions
|
|
13
|
+
class LockManager
|
|
14
|
+
attr_reader :locks, :waiting, :stats
|
|
15
|
+
|
|
16
|
+
def initialize(config = {})
|
|
17
|
+
@locks = {}
|
|
18
|
+
@waiting = {}
|
|
19
|
+
@lock_timeout = config[:lock_timeout] || 30
|
|
20
|
+
@deadlock_detection = config[:deadlock_detection] != false
|
|
21
|
+
@transaction_manager = config[:transaction_manager]
|
|
22
|
+
@stats = {
|
|
23
|
+
locks_acquired: 0,
|
|
24
|
+
locks_released: 0,
|
|
25
|
+
lock_waits: 0,
|
|
26
|
+
lock_timeouts: 0,
|
|
27
|
+
deadlocks_detected: 0,
|
|
28
|
+
deadlocks_resolved: 0
|
|
29
|
+
}
|
|
30
|
+
@lock = Mutex.new
|
|
31
|
+
@condition = ConditionVariable.new
|
|
32
|
+
@transactions = {}
|
|
33
|
+
@deadlock_victims = {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def acquire_lock(transaction, table_name, row_id, lock_type, timeout = @lock_timeout)
|
|
37
|
+
@lock.synchronize do
|
|
38
|
+
@transactions[transaction.id] = transaction
|
|
39
|
+
return false unless transaction.active?
|
|
40
|
+
|
|
41
|
+
key = lock_key(table_name, row_id)
|
|
42
|
+
|
|
43
|
+
# Check if transaction already has lock
|
|
44
|
+
if has_lock?(transaction, key)
|
|
45
|
+
return upgrade_lock(transaction, key, lock_type)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Check if lock is available
|
|
49
|
+
current_lock = @locks[key]
|
|
50
|
+
|
|
51
|
+
if current_lock.nil?
|
|
52
|
+
# No lock exists - acquire
|
|
53
|
+
@locks[key] = Lock.new(key, lock_type, transaction)
|
|
54
|
+
@stats[:locks_acquired] += 1
|
|
55
|
+
return true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check compatibility
|
|
59
|
+
if compatible?(current_lock, lock_type, transaction)
|
|
60
|
+
# Add transaction to lock
|
|
61
|
+
current_lock.add_holder(transaction, lock_type)
|
|
62
|
+
@stats[:locks_acquired] += 1
|
|
63
|
+
return true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Lock conflict - wait
|
|
67
|
+
@stats[:lock_waits] += 1
|
|
68
|
+
result = wait_for_lock(transaction, key, lock_type, timeout)
|
|
69
|
+
|
|
70
|
+
result
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def release_locks(transaction)
|
|
75
|
+
@lock.synchronize do
|
|
76
|
+
@locks.each do |key, lock|
|
|
77
|
+
if lock.holders.key?(transaction.id)
|
|
78
|
+
lock.remove_holder(transaction)
|
|
79
|
+
@stats[:locks_released] += 1
|
|
80
|
+
|
|
81
|
+
# Remove empty lock
|
|
82
|
+
if lock.holders.empty?
|
|
83
|
+
@locks.delete(key)
|
|
84
|
+
|
|
85
|
+
# Wake up waiting transactions
|
|
86
|
+
wake_waiting_transactions(key)
|
|
87
|
+
@condition.broadcast
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
@transactions.delete(transaction.id)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def release_all_locks
|
|
96
|
+
@lock.synchronize do
|
|
97
|
+
@locks.clear
|
|
98
|
+
@waiting.clear
|
|
99
|
+
@transactions.clear
|
|
100
|
+
@deadlock_victims.clear
|
|
101
|
+
@stats[:locks_released] += 1
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def has_lock?(transaction, key)
|
|
106
|
+
lock = @locks[key]
|
|
107
|
+
return false unless lock
|
|
108
|
+
lock.holders.key?(transaction.id)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def lock_type(transaction, key)
|
|
112
|
+
lock = @locks[key]
|
|
113
|
+
return nil unless lock
|
|
114
|
+
lock.holders[transaction.id] if lock.holders.key?(transaction.id)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def waiting_transactions
|
|
118
|
+
@waiting.size
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def deadlock_victim?(transaction_id)
|
|
122
|
+
@lock.synchronize { @deadlock_victims.key?(transaction_id) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def clear_deadlock_victim(transaction_id)
|
|
126
|
+
@lock.synchronize { @deadlock_victims.delete(transaction_id) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def total_locks
|
|
130
|
+
@locks.size
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def lock_info
|
|
134
|
+
@locks.transform_values do |lock|
|
|
135
|
+
{
|
|
136
|
+
type: lock.type,
|
|
137
|
+
holders: lock.holders.keys,
|
|
138
|
+
waiters: lock.waiters.keys
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Return the current wait-for graph as transaction ids. This is a
|
|
144
|
+
# snapshot intended for monitoring and transaction-manager deadlock
|
|
145
|
+
# resolution; callers never receive mutable lock-manager state.
|
|
146
|
+
def wait_for_graph
|
|
147
|
+
@lock.synchronize { build_wait_for_graph }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def build_wait_for_graph
|
|
153
|
+
graph = {}
|
|
154
|
+
@waiting.each do |transaction_id, waits|
|
|
155
|
+
graph[transaction_id] = Set.new
|
|
156
|
+
waits.each_key do |key|
|
|
157
|
+
@locks[key]&.holders&.each_key do |holder_id|
|
|
158
|
+
graph[holder_id] ||= Set.new
|
|
159
|
+
graph[transaction_id] << holder_id
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
graph
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def lock_key(table_name, row_id)
|
|
167
|
+
"#{table_name}:#{row_id}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def compatible?(lock, lock_type, transaction)
|
|
171
|
+
# Check if any holder has incompatible lock
|
|
172
|
+
lock.holders.each do |holder_id, holder_type|
|
|
173
|
+
next if holder_id == transaction.id
|
|
174
|
+
|
|
175
|
+
if !compatible_lock_types(holder_type, lock_type)
|
|
176
|
+
return false
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
true
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def compatible_lock_types(type1, type2)
|
|
184
|
+
# Shared locks are compatible with shared locks
|
|
185
|
+
if type1 == :shared && type2 == :shared
|
|
186
|
+
return true
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Exclusive locks are incompatible with any other
|
|
190
|
+
if type1 == :exclusive || type2 == :exclusive
|
|
191
|
+
return false
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
true
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def upgrade_lock(transaction, key, new_type)
|
|
198
|
+
lock = @locks[key]
|
|
199
|
+
current_type = lock.holders[transaction.id]
|
|
200
|
+
|
|
201
|
+
# Check if upgrade is needed
|
|
202
|
+
return true if current_type == new_type
|
|
203
|
+
|
|
204
|
+
# Check if upgrade is possible
|
|
205
|
+
if new_type == :exclusive && current_type == :shared
|
|
206
|
+
# Need to check if other transactions hold shared locks
|
|
207
|
+
if lock.holders.size == 1 && lock.holders.key?(transaction.id)
|
|
208
|
+
# Only this transaction holds the lock - upgrade
|
|
209
|
+
lock.holders[transaction.id] = :exclusive
|
|
210
|
+
@stats[:locks_acquired] += 1
|
|
211
|
+
return true
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
false
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def wait_for_lock(transaction, key, lock_type, timeout)
|
|
219
|
+
start_time = Time.now
|
|
220
|
+
|
|
221
|
+
# Add to waiters
|
|
222
|
+
@waiting[transaction.id] ||= {}
|
|
223
|
+
@waiting[transaction.id][key] = {
|
|
224
|
+
lock_type: lock_type,
|
|
225
|
+
start_time: start_time
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# Wait loop
|
|
229
|
+
while (remaining = timeout - (Time.now - start_time)) > 0
|
|
230
|
+
if !transaction.active? || @deadlock_victims.key?(transaction.id)
|
|
231
|
+
waits = @waiting[transaction.id]
|
|
232
|
+
waits&.delete(key)
|
|
233
|
+
@waiting.delete(transaction.id) if waits&.empty?
|
|
234
|
+
return false
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Check if lock is available
|
|
238
|
+
if @locks[key].nil? || compatible?(@locks[key], lock_type, transaction)
|
|
239
|
+
# Remove from waiting
|
|
240
|
+
@waiting[transaction.id].delete(key)
|
|
241
|
+
@waiting.delete(transaction.id) if @waiting[transaction.id].empty?
|
|
242
|
+
|
|
243
|
+
# Acquire lock
|
|
244
|
+
@locks[key] ||= Lock.new(key, lock_type)
|
|
245
|
+
@locks[key].add_holder(transaction, lock_type)
|
|
246
|
+
@stats[:locks_acquired] += 1
|
|
247
|
+
return true
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# ConditionVariable releases @lock while waiting, allowing the
|
|
251
|
+
# current holder to release its lock and wake this waiter.
|
|
252
|
+
@condition.wait(@lock, remaining)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Detect while the timed-out waiter is still in the wait graph. The
|
|
256
|
+
# previous ordering removed it first, making a two-transaction cycle
|
|
257
|
+
# impossible to observe. Resolution is performed by the unlocked
|
|
258
|
+
# helper because this method already owns @lock.
|
|
259
|
+
detect_deadlock if @deadlock_detection
|
|
260
|
+
|
|
261
|
+
# Timeout or deadlock victim
|
|
262
|
+
@stats[:lock_timeouts] += 1
|
|
263
|
+
waits = @waiting[transaction.id]
|
|
264
|
+
waits&.delete(key)
|
|
265
|
+
@waiting.delete(transaction.id) if waits&.empty?
|
|
266
|
+
false
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def wake_waiting_transactions(key)
|
|
270
|
+
# Find waiting transactions for this key
|
|
271
|
+
@waiting.each do |txn_id, waits|
|
|
272
|
+
if waits.key?(key)
|
|
273
|
+
# Transaction is waiting - it will be woken in wait loop
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def detect_deadlock
|
|
279
|
+
graph = build_wait_for_graph
|
|
280
|
+
|
|
281
|
+
# Detect cycles
|
|
282
|
+
cycles = detect_cycles(graph)
|
|
283
|
+
|
|
284
|
+
if cycles.any?
|
|
285
|
+
@stats[:deadlocks_detected] += 1
|
|
286
|
+
# Resolve by aborting the lowest-priority transaction. The victim is
|
|
287
|
+
# marked before locks are released so its waiter cannot reacquire a
|
|
288
|
+
# lock after the condition variable wakes it.
|
|
289
|
+
cycles.each do |cycle|
|
|
290
|
+
victim = cycle.compact.min_by { |id| [@transactions[id]&.priority.to_i, id.to_s] }
|
|
291
|
+
abort_transaction_unlocked(victim) if victim
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def detect_cycles(graph)
|
|
297
|
+
cycles = []
|
|
298
|
+
visited = Set.new
|
|
299
|
+
active = Set.new
|
|
300
|
+
path = []
|
|
301
|
+
|
|
302
|
+
graph.keys.each do |node|
|
|
303
|
+
detect_cycle_dfs(node, graph, visited, active, path, cycles) unless visited.include?(node)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
cycles
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def detect_cycle_dfs(node, graph, visited, active, path, cycles)
|
|
310
|
+
if active.include?(node)
|
|
311
|
+
start = path.index(node)
|
|
312
|
+
cycle = path[start..] + [node]
|
|
313
|
+
cycles << cycle unless cycles.any? { |existing| existing == cycle }
|
|
314
|
+
return
|
|
315
|
+
end
|
|
316
|
+
return if visited.include?(node)
|
|
317
|
+
|
|
318
|
+
visited.add(node)
|
|
319
|
+
active.add(node)
|
|
320
|
+
path << node
|
|
321
|
+
|
|
322
|
+
graph[node]&.each do |neighbor|
|
|
323
|
+
detect_cycle_dfs(neighbor, graph, visited, active, path, cycles)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
path.pop
|
|
327
|
+
active.delete(node)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def abort_transaction(transaction_id)
|
|
331
|
+
transaction = if @transaction_manager.respond_to?(:get_transaction)
|
|
332
|
+
@transaction_manager.get_transaction(transaction_id)
|
|
333
|
+
end
|
|
334
|
+
if transaction && @transaction_manager.respond_to?(:rollback_transaction)
|
|
335
|
+
@transaction_manager.rollback_transaction(transaction)
|
|
336
|
+
end
|
|
337
|
+
@lock.synchronize { abort_transaction_unlocked(transaction_id) }
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Called only while @lock is held. It must never call back into the
|
|
341
|
+
# transaction manager, whose own mutex may be held by the caller.
|
|
342
|
+
def abort_transaction_unlocked(transaction_id)
|
|
343
|
+
transaction = @transactions[transaction_id]
|
|
344
|
+
if @transaction_manager
|
|
345
|
+
@deadlock_victims[transaction_id] = transaction if transaction
|
|
346
|
+
else
|
|
347
|
+
transaction&.abort
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
@waiting.delete(transaction_id)
|
|
351
|
+
@locks.each do |key, lock|
|
|
352
|
+
next unless lock.holders.key?(transaction_id)
|
|
353
|
+
|
|
354
|
+
lock.remove_holder(Transaction.new(id: transaction_id))
|
|
355
|
+
@locks.delete(key) if lock.holders.empty?
|
|
356
|
+
end
|
|
357
|
+
@stats[:deadlocks_resolved] += 1
|
|
358
|
+
@condition.broadcast
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyDB
|
|
4
|
+
module Transactions
|
|
5
|
+
|
|
6
|
+
# Import transaction dependency
|
|
7
|
+
require_relative "transaction"
|
|
8
|
+
# Savepoint - Represents a savepoint within a transaction
|
|
9
|
+
class Savepoint
|
|
10
|
+
attr_reader :name, :transaction, :created_at, :position
|
|
11
|
+
attr_reader :modified_rows, :locked_rows, :changes
|
|
12
|
+
|
|
13
|
+
def initialize(name, transaction)
|
|
14
|
+
@name = name
|
|
15
|
+
@transaction = transaction
|
|
16
|
+
@created_at = Time.now
|
|
17
|
+
@position = transaction.savepoints.size
|
|
18
|
+
@modified_rows = {}
|
|
19
|
+
@locked_rows = {}
|
|
20
|
+
@changes = []
|
|
21
|
+
@state = :active
|
|
22
|
+
@lock = Mutex.new
|
|
23
|
+
|
|
24
|
+
# Save current state
|
|
25
|
+
save_state
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def active?
|
|
29
|
+
@state == :active
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def released?
|
|
33
|
+
@state == :released
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def rollbacked?
|
|
37
|
+
@state == :rollbacked
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def rollback
|
|
41
|
+
@lock.synchronize do
|
|
42
|
+
return false unless active?
|
|
43
|
+
|
|
44
|
+
# Rollback changes in reverse order
|
|
45
|
+
@changes.reverse.each do |change|
|
|
46
|
+
undo_change(change)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@state = :rollbacked
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def release
|
|
55
|
+
@lock.synchronize do
|
|
56
|
+
return false unless active?
|
|
57
|
+
@state = :released
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def record_change(change)
|
|
63
|
+
@lock.synchronize do
|
|
64
|
+
@changes << change
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def record_modified_row(table_name, row_id, old_value, new_value)
|
|
69
|
+
@lock.synchronize do
|
|
70
|
+
@modified_rows[row_id] = {
|
|
71
|
+
table: table_name,
|
|
72
|
+
old: old_value,
|
|
73
|
+
new: new_value
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def record_locked_row(table_name, row_id, lock_type)
|
|
79
|
+
@lock.synchronize do
|
|
80
|
+
key = "#{table_name}:#{row_id}"
|
|
81
|
+
@locked_rows[key] = {
|
|
82
|
+
table: table_name,
|
|
83
|
+
row_id: row_id,
|
|
84
|
+
lock_type: lock_type
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def to_hash
|
|
90
|
+
{
|
|
91
|
+
name: @name,
|
|
92
|
+
position: @position,
|
|
93
|
+
created_at: @created_at.iso8601,
|
|
94
|
+
state: @state,
|
|
95
|
+
changes: @changes.size,
|
|
96
|
+
modified_rows: @modified_rows.size,
|
|
97
|
+
locked_rows: @locked_rows.size
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def inspect
|
|
102
|
+
"#<Savepoint name=#{@name} position=#{@position} state=#{@state}>"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def save_state
|
|
108
|
+
# Save current transaction state
|
|
109
|
+
@transaction.modified_rows.each do |row_id, info|
|
|
110
|
+
@modified_rows[row_id] = info.dup
|
|
111
|
+
end
|
|
112
|
+
@transaction.locked_rows.each do |key, info|
|
|
113
|
+
@locked_rows[key] = info.dup
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def undo_change(change)
|
|
118
|
+
case change[:type]
|
|
119
|
+
when :insert
|
|
120
|
+
# Delete inserted row
|
|
121
|
+
@transaction.instance_variable_get(:@engine).delete_row(
|
|
122
|
+
change[:table],
|
|
123
|
+
change[:row_id]
|
|
124
|
+
)
|
|
125
|
+
when :update
|
|
126
|
+
# Restore old values
|
|
127
|
+
@transaction.instance_variable_get(:@engine).update_row(
|
|
128
|
+
change[:table],
|
|
129
|
+
change[:row_id],
|
|
130
|
+
change[:old_values]
|
|
131
|
+
)
|
|
132
|
+
when :delete
|
|
133
|
+
# Reinsert deleted row
|
|
134
|
+
@transaction.instance_variable_get(:@engine).insert_row(
|
|
135
|
+
change[:table],
|
|
136
|
+
change[:columns],
|
|
137
|
+
change[:row_data]
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Import dependencies
|
|
8
|
+
require_relative "savepoint" # For Savepoint class
|
|
9
|
+
require_relative "transaction_id"
|
|
10
|
+
|
|
11
|
+
module RubyDB
|
|
12
|
+
module Transactions
|
|
13
|
+
# Transaction - Represents a database transaction
|
|
14
|
+
class Transaction
|
|
15
|
+
attr_reader :id, :start_time, :status, :isolation_level, :read_only, :priority
|
|
16
|
+
attr_reader :timeout
|
|
17
|
+
attr_reader :savepoints, :locked_rows, :modified_rows, :accessed_tables
|
|
18
|
+
attr_reader :changes
|
|
19
|
+
attr_accessor :parent_transaction
|
|
20
|
+
|
|
21
|
+
# Transaction statuses
|
|
22
|
+
STATUS_ACTIVE = :active
|
|
23
|
+
STATUS_COMMITTED = :committed
|
|
24
|
+
STATUS_ABORTED = :aborted
|
|
25
|
+
STATUS_PREPARED = :prepared
|
|
26
|
+
STATUS_IN_DOUBT = :in_doubt
|
|
27
|
+
|
|
28
|
+
def initialize(options = {})
|
|
29
|
+
@id = options[:id] || generate_id
|
|
30
|
+
@start_time = Time.now
|
|
31
|
+
@status = STATUS_ACTIVE
|
|
32
|
+
@isolation_level = options[:isolation_level] || :read_committed
|
|
33
|
+
@read_only = options[:read_only] || false
|
|
34
|
+
@parent_transaction = options[:parent]
|
|
35
|
+
@savepoints = []
|
|
36
|
+
@locked_rows = {}
|
|
37
|
+
@modified_rows = {}
|
|
38
|
+
@accessed_tables = Set.new
|
|
39
|
+
@changes = []
|
|
40
|
+
@undo_log = []
|
|
41
|
+
@timeout = options[:timeout] || 30 # 30 seconds default
|
|
42
|
+
@deadline = @start_time + @timeout
|
|
43
|
+
@lock = Mutex.new
|
|
44
|
+
@name = options[:name]
|
|
45
|
+
@priority = options[:priority] || 0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def active?
|
|
49
|
+
@status == STATUS_ACTIVE
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def committed?
|
|
53
|
+
@status == STATUS_COMMITTED
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def aborted?
|
|
57
|
+
@status == STATUS_ABORTED
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def prepared?
|
|
61
|
+
@status == STATUS_PREPARED
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def in_doubt?
|
|
65
|
+
@status == STATUS_IN_DOUBT
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def expired?
|
|
69
|
+
Time.now > @deadline
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def add_change(change)
|
|
73
|
+
@lock.synchronize do
|
|
74
|
+
@changes << change
|
|
75
|
+
@undo_log << create_undo_entry(change)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def add_modified_row(table_name, row_id, old_value, new_value)
|
|
80
|
+
@lock.synchronize do
|
|
81
|
+
@modified_rows[row_id] ||= {
|
|
82
|
+
table: table_name,
|
|
83
|
+
old: old_value,
|
|
84
|
+
new: new_value,
|
|
85
|
+
timestamp: Time.now
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def add_locked_row(table_name, row_id, lock_type)
|
|
91
|
+
@lock.synchronize do
|
|
92
|
+
key = "#{table_name}:#{row_id}"
|
|
93
|
+
@locked_rows[key] = {
|
|
94
|
+
table: table_name,
|
|
95
|
+
row_id: row_id,
|
|
96
|
+
lock_type: lock_type,
|
|
97
|
+
timestamp: Time.now
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def add_accessed_table(table_name)
|
|
103
|
+
@lock.synchronize do
|
|
104
|
+
@accessed_tables.add(table_name)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def create_savepoint(name = nil)
|
|
109
|
+
@lock.synchronize do
|
|
110
|
+
savepoint = Savepoint.new(name || "savepoint_#{@savepoints.size + 1}", self)
|
|
111
|
+
@savepoints << savepoint
|
|
112
|
+
savepoint
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def rollback_to_savepoint(name)
|
|
117
|
+
@lock.synchronize do
|
|
118
|
+
index = @savepoints.index { |sp| sp.name == name }
|
|
119
|
+
return false unless index
|
|
120
|
+
|
|
121
|
+
# Rollback changes after this savepoint
|
|
122
|
+
@savepoints[index + 1..-1].each do |sp|
|
|
123
|
+
sp.rollback
|
|
124
|
+
end
|
|
125
|
+
@savepoints = @savepoints[0..index]
|
|
126
|
+
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def release_savepoint(name)
|
|
132
|
+
@lock.synchronize do
|
|
133
|
+
index = @savepoints.index { |sp| sp.name == name }
|
|
134
|
+
return false unless index
|
|
135
|
+
|
|
136
|
+
@savepoints.delete_at(index)
|
|
137
|
+
true
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def commit
|
|
142
|
+
@lock.synchronize do
|
|
143
|
+
return false unless active?
|
|
144
|
+
@status = STATUS_COMMITTED
|
|
145
|
+
@commit_time = Time.now
|
|
146
|
+
true
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def abort
|
|
151
|
+
@lock.synchronize do
|
|
152
|
+
return false unless active?
|
|
153
|
+
@status = STATUS_ABORTED
|
|
154
|
+
@abort_time = Time.now
|
|
155
|
+
true
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def prepare
|
|
160
|
+
@lock.synchronize do
|
|
161
|
+
return false unless active?
|
|
162
|
+
@status = STATUS_PREPARED
|
|
163
|
+
@prepare_time = Time.now
|
|
164
|
+
true
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def mark_in_doubt
|
|
169
|
+
@lock.synchronize do
|
|
170
|
+
@status = STATUS_IN_DOUBT
|
|
171
|
+
@in_doubt_time = Time.now
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def to_hash
|
|
176
|
+
{
|
|
177
|
+
id: @id,
|
|
178
|
+
start_time: @start_time.iso8601,
|
|
179
|
+
status: @status,
|
|
180
|
+
isolation_level: @isolation_level,
|
|
181
|
+
read_only: @read_only,
|
|
182
|
+
changes: @changes.size,
|
|
183
|
+
locked_rows: @locked_rows.size,
|
|
184
|
+
modified_rows: @modified_rows.size,
|
|
185
|
+
accessed_tables: @accessed_tables.to_a,
|
|
186
|
+
savepoints: @savepoints.map(&:name),
|
|
187
|
+
timeout: @timeout,
|
|
188
|
+
expired: expired?
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def inspect
|
|
193
|
+
"#<Transaction id=#{@id} status=#{@status} isolation=#{@isolation_level}>"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def to_s
|
|
197
|
+
inspect
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
def generate_id
|
|
203
|
+
"txn_#{Time.now.to_i}_#{SecureRandom.hex(8)}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def create_undo_entry(change)
|
|
207
|
+
{
|
|
208
|
+
change: change,
|
|
209
|
+
timestamp: Time.now,
|
|
210
|
+
type: change[:type]
|
|
211
|
+
}
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|