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,855 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module RubyDB
|
|
6
|
+
module Storage
|
|
7
|
+
# PageAllocator - Handles page allocation with free space management
|
|
8
|
+
# This is a production-grade implementation with proper fragmentation management,
|
|
9
|
+
# overflow page handling, and comprehensive statistics tracking.
|
|
10
|
+
class PageAllocator
|
|
11
|
+
attr_reader :page_manager, :free_space_map, :stats
|
|
12
|
+
|
|
13
|
+
def initialize(page_manager, buffer_pool: nil)
|
|
14
|
+
@page_manager = page_manager
|
|
15
|
+
@buffer_pool = buffer_pool
|
|
16
|
+
@free_space_map = FreeSpaceMap.new(page_manager)
|
|
17
|
+
@lock = Mutex.new
|
|
18
|
+
@stats = {
|
|
19
|
+
allocations: 0,
|
|
20
|
+
releases: 0,
|
|
21
|
+
compactions: 0,
|
|
22
|
+
defragmentations: 0,
|
|
23
|
+
allocation_failures: 0,
|
|
24
|
+
total_space_requested: 0,
|
|
25
|
+
total_space_allocated: 0,
|
|
26
|
+
total_space_released: 0,
|
|
27
|
+
overflow_allocations: 0,
|
|
28
|
+
overflow_releases: 0
|
|
29
|
+
}
|
|
30
|
+
@overflow_pages = {}
|
|
31
|
+
@page_usage = {}
|
|
32
|
+
@page_records_cache = {}
|
|
33
|
+
@cache_hits = 0
|
|
34
|
+
@cache_misses = 0
|
|
35
|
+
@initialized = true
|
|
36
|
+
@max_cache_size = 1000
|
|
37
|
+
@defragmentation_threshold = 0.30 # 30% fragmentation triggers defrag
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Allocate a page with at least needed_bytes of space
|
|
41
|
+
def allocate_page_with_space(needed_bytes, page_type = 0)
|
|
42
|
+
@lock.synchronize do
|
|
43
|
+
@stats[:allocations] += 1
|
|
44
|
+
@stats[:total_space_requested] += needed_bytes
|
|
45
|
+
|
|
46
|
+
page_size = @page_manager.file_manager.page_size
|
|
47
|
+
max_record_size = page_size - PageHeader::SIZE - 8 # 8 bytes for record header
|
|
48
|
+
|
|
49
|
+
# Validate request
|
|
50
|
+
if needed_bytes > max_record_size
|
|
51
|
+
@stats[:allocation_failures] += 1
|
|
52
|
+
raise StorageError, "Requested space #{needed_bytes} exceeds maximum record size #{max_record_size}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if needed_bytes <= 0
|
|
56
|
+
@stats[:allocation_failures] += 1
|
|
57
|
+
raise StorageError, "Requested space must be positive"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
page_number = nil
|
|
61
|
+
page = nil
|
|
62
|
+
|
|
63
|
+
# Try to find existing page with enough free space (best fit)
|
|
64
|
+
candidate_page = @free_space_map.find_page_with_space(needed_bytes)
|
|
65
|
+
|
|
66
|
+
if candidate_page
|
|
67
|
+
page = get_page(candidate_page)
|
|
68
|
+
|
|
69
|
+
# Verify page is valid and has space
|
|
70
|
+
if page && page.header.page_type != 3 && page.free_space >= needed_bytes
|
|
71
|
+
page_number = candidate_page
|
|
72
|
+
else
|
|
73
|
+
# Free space map is stale - update it
|
|
74
|
+
@free_space_map.update_page(candidate_page, page ? page.free_space : 0)
|
|
75
|
+
# Try another search
|
|
76
|
+
page_number = @free_space_map.find_page_with_space(needed_bytes)
|
|
77
|
+
page = get_page(page_number) if page_number
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
unless page_number && page
|
|
82
|
+
# Allocate new page
|
|
83
|
+
page_number = @page_manager.allocate_page(page_type)
|
|
84
|
+
|
|
85
|
+
# Initialize page
|
|
86
|
+
page = Page.new(page_number, page_size)
|
|
87
|
+
page.header.page_type = page_type
|
|
88
|
+
page.header.data_end = PageHeader::SIZE
|
|
89
|
+
page.write_header
|
|
90
|
+
|
|
91
|
+
write_page(page)
|
|
92
|
+
@free_space_map.update_page(page_number, page.free_space)
|
|
93
|
+
|
|
94
|
+
# Clear any cached records for this page
|
|
95
|
+
@page_records_cache.delete(page_number)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Get the page and reserve space
|
|
99
|
+
page = get_page(page_number)
|
|
100
|
+
|
|
101
|
+
# Calculate new data end position
|
|
102
|
+
new_data_end = page.header.data_end + needed_bytes
|
|
103
|
+
|
|
104
|
+
# Double-check we have enough space
|
|
105
|
+
if new_data_end > page_size
|
|
106
|
+
@stats[:allocation_failures] += 1
|
|
107
|
+
raise StorageError, "Page #{page_number} doesn't have enough space (needed: #{needed_bytes}, available: #{page.free_space}, total: #{page_size - new_data_end})"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Update page header
|
|
111
|
+
page.header.data_end = new_data_end
|
|
112
|
+
page.write_header
|
|
113
|
+
|
|
114
|
+
# Write the updated page
|
|
115
|
+
write_page(page)
|
|
116
|
+
|
|
117
|
+
# Update free space map
|
|
118
|
+
@free_space_map.update_page(page_number, page.free_space)
|
|
119
|
+
|
|
120
|
+
# Track page usage
|
|
121
|
+
@page_usage[page_number] ||= {
|
|
122
|
+
allocated: 0,
|
|
123
|
+
freed: 0,
|
|
124
|
+
record_count: 0,
|
|
125
|
+
created_at: Time.now,
|
|
126
|
+
last_modified: Time.now,
|
|
127
|
+
total_writes: 0
|
|
128
|
+
}
|
|
129
|
+
@page_usage[page_number][:allocated] += needed_bytes
|
|
130
|
+
@page_usage[page_number][:record_count] += 1
|
|
131
|
+
@page_usage[page_number][:last_modified] = Time.now
|
|
132
|
+
@page_usage[page_number][:total_writes] += 1
|
|
133
|
+
|
|
134
|
+
@stats[:total_space_allocated] += needed_bytes
|
|
135
|
+
|
|
136
|
+
# Clear cache for this page
|
|
137
|
+
@page_records_cache.delete(page_number)
|
|
138
|
+
|
|
139
|
+
# Return the offset where data can be written
|
|
140
|
+
offset = new_data_end - needed_bytes
|
|
141
|
+
|
|
142
|
+
{
|
|
143
|
+
page_number: page_number,
|
|
144
|
+
offset: offset,
|
|
145
|
+
page: page,
|
|
146
|
+
free_space_remaining: page.free_space,
|
|
147
|
+
page_usage: @page_usage[page_number]
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Release a page back to the free pool
|
|
153
|
+
def release_page(page_number)
|
|
154
|
+
@lock.synchronize do
|
|
155
|
+
@stats[:releases] += 1
|
|
156
|
+
|
|
157
|
+
# Validate page number
|
|
158
|
+
if page_number < 0 || page_number >= @page_manager.total_pages
|
|
159
|
+
raise StorageError, "Invalid page number: #{page_number}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Get page before freeing
|
|
163
|
+
page = get_page(page_number)
|
|
164
|
+
raise StorageError, "Page #{page_number} could not be read" unless page
|
|
165
|
+
|
|
166
|
+
if page
|
|
167
|
+
# Record usage stats before freeing
|
|
168
|
+
usage = @page_usage[page_number]
|
|
169
|
+
if usage
|
|
170
|
+
@stats[:total_space_allocated] -= usage[:allocated]
|
|
171
|
+
@stats[:total_space_released] += usage[:allocated]
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Release any overflow pages first
|
|
176
|
+
released_overflow = release_overflow_pages(page_number)
|
|
177
|
+
if released_overflow > 0
|
|
178
|
+
@stats[:overflow_releases] += released_overflow
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Free the page
|
|
182
|
+
@page_manager.free_page(page_number)
|
|
183
|
+
@free_space_map.remove_page(page_number)
|
|
184
|
+
@page_usage.delete(page_number)
|
|
185
|
+
@page_records_cache.delete(page_number)
|
|
186
|
+
|
|
187
|
+
true
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Get a page and verify it has enough space
|
|
192
|
+
def get_page_with_space(page_number, needed_bytes)
|
|
193
|
+
@lock.synchronize do
|
|
194
|
+
# Validate page exists
|
|
195
|
+
unless page_number < @page_manager.total_pages
|
|
196
|
+
raise StorageError, "Page #{page_number} does not exist"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
page = get_page(page_number)
|
|
200
|
+
|
|
201
|
+
# Check if page is free
|
|
202
|
+
if page.header.page_type == 3
|
|
203
|
+
raise StorageError, "Page #{page_number} is marked as free"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Check if page has enough free space
|
|
207
|
+
if page.free_space < needed_bytes
|
|
208
|
+
# Try to compact the page first
|
|
209
|
+
compact_result = compact_page(page_number)
|
|
210
|
+
|
|
211
|
+
# Re-check after compaction
|
|
212
|
+
page = get_page(page_number)
|
|
213
|
+
|
|
214
|
+
if page.free_space < needed_bytes
|
|
215
|
+
# Check if we can allocate an overflow page
|
|
216
|
+
needed_overflow = needed_bytes - page.free_space
|
|
217
|
+
overflow_page = allocate_overflow_page(page_number, needed_overflow)
|
|
218
|
+
|
|
219
|
+
if overflow_page
|
|
220
|
+
@stats[:overflow_allocations] += 1
|
|
221
|
+
return {
|
|
222
|
+
page_number: overflow_page.page_number,
|
|
223
|
+
page: overflow_page,
|
|
224
|
+
overflow: true,
|
|
225
|
+
parent_page: page_number
|
|
226
|
+
}
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
raise StorageError, "Not enough space on page #{page_number} (needed: #{needed_bytes}, available: #{page.free_space}, freed: #{compact_result[:space_reclaimed]})"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
{ page_number: page_number, page: page, overflow: false }
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Compact a page to remove holes and fragmentation
|
|
238
|
+
def compact_page(page_number)
|
|
239
|
+
@lock.synchronize do
|
|
240
|
+
@stats[:compactions] += 1
|
|
241
|
+
|
|
242
|
+
# Validate page
|
|
243
|
+
unless page_number < @page_manager.total_pages
|
|
244
|
+
raise StorageError, "Page #{page_number} does not exist"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
page = get_page(page_number)
|
|
248
|
+
|
|
249
|
+
# Skip if page is free
|
|
250
|
+
if page.header.page_type == 3
|
|
251
|
+
return { page_number: page_number, compacted: false, reason: "page is free" }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
header_size = PageHeader::SIZE
|
|
255
|
+
old_data_end = page.header.data_end
|
|
256
|
+
|
|
257
|
+
# Get all records on the page with their actual data
|
|
258
|
+
records = read_page_records(page)
|
|
259
|
+
|
|
260
|
+
if records.empty?
|
|
261
|
+
# No records to compact
|
|
262
|
+
return {
|
|
263
|
+
page_number: page_number,
|
|
264
|
+
compacted: false,
|
|
265
|
+
reason: "no records",
|
|
266
|
+
space_reclaimed: 0,
|
|
267
|
+
records_compacted: 0
|
|
268
|
+
}
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Sort records by offset to compact them
|
|
272
|
+
records.sort_by! { |r| r[:offset] }
|
|
273
|
+
|
|
274
|
+
# Check if compaction is needed (check for gaps)
|
|
275
|
+
needs_compaction = false
|
|
276
|
+
current_offset = header_size
|
|
277
|
+
|
|
278
|
+
records.each do |record|
|
|
279
|
+
if record[:offset] > current_offset
|
|
280
|
+
needs_compaction = true
|
|
281
|
+
break
|
|
282
|
+
end
|
|
283
|
+
current_offset += record[:length] + record[:header_size]
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
unless needs_compaction
|
|
287
|
+
return {
|
|
288
|
+
page_number: page_number,
|
|
289
|
+
compacted: false,
|
|
290
|
+
reason: "no fragmentation",
|
|
291
|
+
space_reclaimed: 0,
|
|
292
|
+
records_compacted: records.size
|
|
293
|
+
}
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Rewrite records contiguously
|
|
297
|
+
current_offset = header_size
|
|
298
|
+
compacted_records = []
|
|
299
|
+
|
|
300
|
+
records.each do |record|
|
|
301
|
+
# Read the full record data including header
|
|
302
|
+
record_data = page.read(record[:offset], record[:header_size] + record[:length])
|
|
303
|
+
|
|
304
|
+
# Write to new position
|
|
305
|
+
page.write(current_offset, record_data)
|
|
306
|
+
|
|
307
|
+
# Store mapping for potential updates
|
|
308
|
+
compacted_records << {
|
|
309
|
+
old_offset: record[:offset],
|
|
310
|
+
new_offset: current_offset,
|
|
311
|
+
length: record[:length],
|
|
312
|
+
header_size: record[:header_size]
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
current_offset += record[:header_size] + record[:length]
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Update data_end to the new end position
|
|
319
|
+
new_data_end = current_offset
|
|
320
|
+
page.header.data_end = new_data_end
|
|
321
|
+
page.write_header
|
|
322
|
+
|
|
323
|
+
write_page(page)
|
|
324
|
+
|
|
325
|
+
# Update free space map
|
|
326
|
+
@free_space_map.update_page(page_number, page.free_space)
|
|
327
|
+
|
|
328
|
+
# Clear cache for this page
|
|
329
|
+
@page_records_cache.delete(page_number)
|
|
330
|
+
|
|
331
|
+
# Track compaction in page usage
|
|
332
|
+
if @page_usage[page_number]
|
|
333
|
+
@page_usage[page_number][:compactions] = (@page_usage[page_number][:compactions] || 0) + 1
|
|
334
|
+
@page_usage[page_number][:space_reclaimed] = (@page_usage[page_number][:space_reclaimed] || 0) + (old_data_end - new_data_end)
|
|
335
|
+
@page_usage[page_number][:last_compact] = Time.now
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Return compaction results
|
|
339
|
+
{
|
|
340
|
+
page_number: page_number,
|
|
341
|
+
compacted: true,
|
|
342
|
+
old_end: old_data_end,
|
|
343
|
+
new_end: new_data_end,
|
|
344
|
+
records_compacted: records.size,
|
|
345
|
+
space_reclaimed: old_data_end - new_data_end,
|
|
346
|
+
compacted_records: compacted_records
|
|
347
|
+
}
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Allocate an overflow page for large records
|
|
352
|
+
def allocate_overflow_page(parent_page_number, needed_bytes)
|
|
353
|
+
@lock.synchronize do
|
|
354
|
+
page_size = @page_manager.file_manager.page_size
|
|
355
|
+
|
|
356
|
+
# Validate request
|
|
357
|
+
if needed_bytes > page_size - PageHeader::SIZE - 8
|
|
358
|
+
raise StorageError, "Overflow request #{needed_bytes} exceeds page capacity"
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Allocate new page for overflow
|
|
362
|
+
overflow_page_number = @page_manager.allocate_page(4) # PAGE_TYPE_OVERFLOW
|
|
363
|
+
|
|
364
|
+
# Initialize overflow page
|
|
365
|
+
overflow_page = Page.new(overflow_page_number, page_size)
|
|
366
|
+
overflow_page.header.page_type = 4 # PAGE_TYPE_OVERFLOW
|
|
367
|
+
overflow_page.header.data_end = PageHeader::SIZE + needed_bytes
|
|
368
|
+
overflow_page.header.next_page = 0
|
|
369
|
+
overflow_page.header.prev_page = parent_page_number
|
|
370
|
+
overflow_page.write_header
|
|
371
|
+
|
|
372
|
+
write_page(overflow_page)
|
|
373
|
+
|
|
374
|
+
# Link overflow page to parent
|
|
375
|
+
@overflow_pages[parent_page_number] ||= []
|
|
376
|
+
@overflow_pages[parent_page_number] << overflow_page_number
|
|
377
|
+
|
|
378
|
+
# Track overflow page usage
|
|
379
|
+
@page_usage[overflow_page_number] = {
|
|
380
|
+
allocated: needed_bytes,
|
|
381
|
+
freed: 0,
|
|
382
|
+
record_count: 0,
|
|
383
|
+
created_at: Time.now,
|
|
384
|
+
last_modified: Time.now,
|
|
385
|
+
total_writes: 1,
|
|
386
|
+
is_overflow: true,
|
|
387
|
+
parent_page: parent_page_number
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# Update free space map
|
|
391
|
+
@free_space_map.update_page(overflow_page_number, overflow_page.free_space)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
overflow_page
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Get overflow pages for a page
|
|
399
|
+
def get_overflow_pages(page_number)
|
|
400
|
+
@lock.synchronize do
|
|
401
|
+
@overflow_pages[page_number] || []
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Get all overflow page information
|
|
406
|
+
def all_overflow_info
|
|
407
|
+
@lock.synchronize do
|
|
408
|
+
result = {}
|
|
409
|
+
@overflow_pages.each do |parent, children|
|
|
410
|
+
result[parent] = children.map do |child|
|
|
411
|
+
page = get_page(child) rescue nil
|
|
412
|
+
if page
|
|
413
|
+
{
|
|
414
|
+
page_number: child,
|
|
415
|
+
size: page.header.data_end - PageHeader::SIZE,
|
|
416
|
+
prev_page: page.header.prev_page,
|
|
417
|
+
next_page: page.header.next_page
|
|
418
|
+
}
|
|
419
|
+
else
|
|
420
|
+
{ page_number: child, valid: false }
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
result
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Release all overflow pages for a page
|
|
429
|
+
def release_overflow_pages(page_number)
|
|
430
|
+
@lock.synchronize do
|
|
431
|
+
overflow_pages = @overflow_pages[page_number] || []
|
|
432
|
+
|
|
433
|
+
released = 0
|
|
434
|
+
errors = []
|
|
435
|
+
overflow_pages.each do |overflow_page_number|
|
|
436
|
+
begin
|
|
437
|
+
@page_manager.free_page(overflow_page_number)
|
|
438
|
+
@free_space_map.remove_page(overflow_page_number)
|
|
439
|
+
@page_usage.delete(overflow_page_number)
|
|
440
|
+
@page_records_cache.delete(overflow_page_number)
|
|
441
|
+
released += 1
|
|
442
|
+
rescue => e
|
|
443
|
+
errors << [overflow_page_number, e]
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
unless errors.empty?
|
|
448
|
+
details = errors.map { |page_number, error| "#{page_number}: #{error.message}" }.join(", ")
|
|
449
|
+
raise StorageError, "Failed to release overflow pages (#{details})"
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
@overflow_pages.delete(page_number)
|
|
453
|
+
|
|
454
|
+
released
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Get total free space across all pages
|
|
459
|
+
def total_free_space
|
|
460
|
+
@free_space_map.total_free_space
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Get free space on a specific page
|
|
464
|
+
def page_free_space(page_number)
|
|
465
|
+
@free_space_map.get_free_space(page_number)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Get page usage statistics
|
|
469
|
+
def page_usage(page_number)
|
|
470
|
+
@page_usage[page_number] || { allocated: 0, freed: 0, record_count: 0 }
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Get all page usage statistics
|
|
474
|
+
def all_page_usage
|
|
475
|
+
@page_usage.dup
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Calculate fragmentation percentage for a page
|
|
479
|
+
def page_fragmentation(page_number)
|
|
480
|
+
page = get_page(page_number) rescue nil
|
|
481
|
+
return 0.0 unless page
|
|
482
|
+
|
|
483
|
+
# Skip if page is free
|
|
484
|
+
return 0.0 if page.header.page_type == 3
|
|
485
|
+
|
|
486
|
+
# Get all records on the page
|
|
487
|
+
records = read_page_records(page)
|
|
488
|
+
return 0.0 if records.empty?
|
|
489
|
+
|
|
490
|
+
# Calculate total record data size including headers
|
|
491
|
+
total_data = records.sum { |r| r[:header_size] + r[:length] }
|
|
492
|
+
|
|
493
|
+
# Calculate fragmentation (holes between records)
|
|
494
|
+
header_size = PageHeader::SIZE
|
|
495
|
+
sorted_records = records.sort_by { |r| r[:offset] }
|
|
496
|
+
|
|
497
|
+
fragmentation = 0.0
|
|
498
|
+
last_end = header_size
|
|
499
|
+
|
|
500
|
+
sorted_records.each do |record|
|
|
501
|
+
if record[:offset] > last_end
|
|
502
|
+
fragmentation += record[:offset] - last_end
|
|
503
|
+
end
|
|
504
|
+
last_end = record[:offset] + record[:header_size] + record[:length]
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Add space after last record
|
|
508
|
+
if last_end < page.header.data_end
|
|
509
|
+
fragmentation += page.header.data_end - last_end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
total_used = page.header.data_end - header_size
|
|
513
|
+
return 0.0 if total_used == 0
|
|
514
|
+
|
|
515
|
+
(fragmentation.to_f / total_used * 100).round(2)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Defragment a page (compact and reorganize)
|
|
519
|
+
def defragment_page(page_number)
|
|
520
|
+
@lock.synchronize do
|
|
521
|
+
@stats[:defragmentations] += 1
|
|
522
|
+
|
|
523
|
+
# Compact the page
|
|
524
|
+
compaction_result = compact_page(page_number)
|
|
525
|
+
|
|
526
|
+
# Update page usage
|
|
527
|
+
if @page_usage[page_number]
|
|
528
|
+
@page_usage[page_number][:defragmented_at] = Time.now
|
|
529
|
+
@page_usage[page_number][:defragment_count] = (@page_usage[page_number][:defragment_count] || 0) + 1
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
{
|
|
533
|
+
page_number: page_number,
|
|
534
|
+
compacted: compaction_result[:compacted],
|
|
535
|
+
space_reclaimed: compaction_result[:space_reclaimed],
|
|
536
|
+
freed_overflow_pages: 0,
|
|
537
|
+
fragmentation_before: compaction_result[:fragmentation_before] || 0,
|
|
538
|
+
fragmentation_after: page_fragmentation(page_number)
|
|
539
|
+
}
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Reserve space on a page for writing
|
|
544
|
+
def reserve_space(page_number, needed_bytes)
|
|
545
|
+
@lock.synchronize do
|
|
546
|
+
# Validate page
|
|
547
|
+
unless page_number < @page_manager.total_pages
|
|
548
|
+
raise StorageError, "Page #{page_number} does not exist"
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
page = get_page(page_number)
|
|
552
|
+
|
|
553
|
+
# Skip if page is free
|
|
554
|
+
if page.header.page_type == 3
|
|
555
|
+
raise StorageError, "Cannot reserve space on free page #{page_number}"
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Check if we need to compact or allocate overflow
|
|
559
|
+
if page.free_space < needed_bytes
|
|
560
|
+
# Try compacting first
|
|
561
|
+
compact_result = compact_page(page_number)
|
|
562
|
+
page = get_page(page_number)
|
|
563
|
+
|
|
564
|
+
if page.free_space < needed_bytes
|
|
565
|
+
# Try allocating overflow
|
|
566
|
+
overflow_page = allocate_overflow_page(page_number, needed_bytes - page.free_space)
|
|
567
|
+
if overflow_page
|
|
568
|
+
return {
|
|
569
|
+
page_number: overflow_page.page_number,
|
|
570
|
+
offset: PageHeader::SIZE,
|
|
571
|
+
page: overflow_page,
|
|
572
|
+
is_overflow: true
|
|
573
|
+
}
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
raise StorageError, "Cannot reserve #{needed_bytes} bytes on page #{page_number} (available: #{page.free_space})"
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
# Reserve space on the page
|
|
581
|
+
offset = page.header.data_end
|
|
582
|
+
page.header.data_end += needed_bytes
|
|
583
|
+
page.write_header
|
|
584
|
+
write_page(page)
|
|
585
|
+
|
|
586
|
+
# Update free space map
|
|
587
|
+
@free_space_map.update_page(page_number, page.free_space)
|
|
588
|
+
|
|
589
|
+
# Update page usage
|
|
590
|
+
if @page_usage[page_number]
|
|
591
|
+
@page_usage[page_number][:reserved] = (@page_usage[page_number][:reserved] || 0) + needed_bytes
|
|
592
|
+
@page_usage[page_number][:last_modified] = Time.now
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# Clear cache
|
|
596
|
+
@page_records_cache.delete(page_number)
|
|
597
|
+
|
|
598
|
+
{ page_number: page_number, offset: offset, page: page, is_overflow: false }
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# Release reserved space on a page
|
|
603
|
+
def release_reserved_space(page_number, offset, length)
|
|
604
|
+
@lock.synchronize do
|
|
605
|
+
# Validate page
|
|
606
|
+
unless page_number < @page_manager.total_pages
|
|
607
|
+
return false
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
page = get_page(page_number)
|
|
611
|
+
|
|
612
|
+
# Skip if page is free
|
|
613
|
+
return false if page.header.page_type == 3
|
|
614
|
+
|
|
615
|
+
# Only allow releasing space from the end (where we reserved it)
|
|
616
|
+
if offset + length == page.header.data_end
|
|
617
|
+
page.header.data_end -= length
|
|
618
|
+
page.write_header
|
|
619
|
+
write_page(page)
|
|
620
|
+
|
|
621
|
+
# Update free space map
|
|
622
|
+
@free_space_map.update_page(page_number, page.free_space)
|
|
623
|
+
|
|
624
|
+
# Update page usage
|
|
625
|
+
if @page_usage[page_number]
|
|
626
|
+
@page_usage[page_number][:reserved] = (@page_usage[page_number][:reserved] || 0) - length
|
|
627
|
+
@page_usage[page_number][:released_reserved] = (@page_usage[page_number][:released_reserved] || 0) + length
|
|
628
|
+
@page_usage[page_number][:last_modified] = Time.now
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# Clear cache
|
|
632
|
+
@page_records_cache.delete(page_number)
|
|
633
|
+
|
|
634
|
+
true
|
|
635
|
+
else
|
|
636
|
+
# Cannot release internal space - would need compaction
|
|
637
|
+
false
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# Get page allocation statistics
|
|
643
|
+
def allocation_stats
|
|
644
|
+
@lock.synchronize do
|
|
645
|
+
fragmentation_avg = average_fragmentation
|
|
646
|
+
|
|
647
|
+
{
|
|
648
|
+
total_pages: @page_manager.total_pages,
|
|
649
|
+
free_pages: @page_manager.free_pages,
|
|
650
|
+
used_pages: @page_manager.used_pages,
|
|
651
|
+
page_usage_count: @page_usage.size,
|
|
652
|
+
overflow_pages: @overflow_pages.values.flatten.size,
|
|
653
|
+
overflow_groups: @overflow_pages.size,
|
|
654
|
+
allocations: @stats[:allocations],
|
|
655
|
+
releases: @stats[:releases],
|
|
656
|
+
compactions: @stats[:compactions],
|
|
657
|
+
defragmentations: @stats[:defragmentations],
|
|
658
|
+
allocation_failures: @stats[:allocation_failures],
|
|
659
|
+
overflow_allocations: @stats[:overflow_allocations],
|
|
660
|
+
overflow_releases: @stats[:overflow_releases],
|
|
661
|
+
total_space_requested: @stats[:total_space_requested],
|
|
662
|
+
total_space_allocated: @stats[:total_space_allocated],
|
|
663
|
+
total_space_released: @stats[:total_space_released],
|
|
664
|
+
free_space_total: total_free_space,
|
|
665
|
+
fragmentation_avg: fragmentation_avg,
|
|
666
|
+
cache_hits: @cache_hits,
|
|
667
|
+
cache_misses: @cache_misses,
|
|
668
|
+
cache_hit_rate: cache_hit_rate,
|
|
669
|
+
defragmentation_threshold: @defragmentation_threshold
|
|
670
|
+
}
|
|
671
|
+
end
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# Calculate average fragmentation across all pages
|
|
675
|
+
def average_fragmentation
|
|
676
|
+
pages = @page_manager.total_pages
|
|
677
|
+
return 0.0 if pages == 0
|
|
678
|
+
|
|
679
|
+
total_fragmentation = 0.0
|
|
680
|
+
count = 0
|
|
681
|
+
|
|
682
|
+
(0...pages).each do |page_number|
|
|
683
|
+
begin
|
|
684
|
+
frag = page_fragmentation(page_number)
|
|
685
|
+
if frag > 0
|
|
686
|
+
total_fragmentation += frag
|
|
687
|
+
count += 1
|
|
688
|
+
end
|
|
689
|
+
rescue
|
|
690
|
+
# Skip pages that can't be read
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
count > 0 ? (total_fragmentation / count).round(2) : 0.0
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
# Get pages with high fragmentation (above threshold)
|
|
698
|
+
def fragmented_pages(threshold = @defragmentation_threshold)
|
|
699
|
+
@lock.synchronize do
|
|
700
|
+
result = []
|
|
701
|
+
(0...@page_manager.total_pages).each do |page_number|
|
|
702
|
+
begin
|
|
703
|
+
frag = page_fragmentation(page_number)
|
|
704
|
+
if frag > threshold * 100 # threshold is 0-1, frag is 0-100
|
|
705
|
+
result << {
|
|
706
|
+
page_number: page_number,
|
|
707
|
+
fragmentation: frag,
|
|
708
|
+
free_space: page_free_space(page_number),
|
|
709
|
+
usage: page_usage(page_number)
|
|
710
|
+
}
|
|
711
|
+
end
|
|
712
|
+
rescue
|
|
713
|
+
# Skip pages that can't be read
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
result.sort_by { |p| -p[:fragmentation] }
|
|
717
|
+
end
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
# Automatically defragment pages above threshold
|
|
721
|
+
def auto_defragment(threshold = @defragmentation_threshold)
|
|
722
|
+
@lock.synchronize do
|
|
723
|
+
pages = fragmented_pages(threshold)
|
|
724
|
+
results = []
|
|
725
|
+
|
|
726
|
+
pages.each do |page_info|
|
|
727
|
+
begin
|
|
728
|
+
result = defragment_page(page_info[:page_number])
|
|
729
|
+
results << result
|
|
730
|
+
rescue => e
|
|
731
|
+
results << { page_number: page_info[:page_number], error: e.message }
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
{
|
|
736
|
+
defragmented: results.select { |r| r[:compacted] }.size,
|
|
737
|
+
errors: results.select { |r| r[:error] }.size,
|
|
738
|
+
total_space_reclaimed: results.sum { |r| r[:space_reclaimed] || 0 },
|
|
739
|
+
results: results
|
|
740
|
+
}
|
|
741
|
+
end
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
# Reset statistics
|
|
745
|
+
def reset_stats
|
|
746
|
+
@lock.synchronize do
|
|
747
|
+
@stats = {
|
|
748
|
+
allocations: 0,
|
|
749
|
+
releases: 0,
|
|
750
|
+
compactions: 0,
|
|
751
|
+
defragmentations: 0,
|
|
752
|
+
allocation_failures: 0,
|
|
753
|
+
total_space_requested: 0,
|
|
754
|
+
total_space_allocated: 0,
|
|
755
|
+
total_space_released: 0,
|
|
756
|
+
overflow_allocations: 0,
|
|
757
|
+
overflow_releases: 0
|
|
758
|
+
}
|
|
759
|
+
@cache_hits = 0
|
|
760
|
+
@cache_misses = 0
|
|
761
|
+
end
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
private
|
|
765
|
+
|
|
766
|
+
# Compaction and reservation must observe dirty pages held by the
|
|
767
|
+
# buffer pool. Falling back to PageManager keeps this class usable in
|
|
768
|
+
# isolated tests and by legacy callers.
|
|
769
|
+
def get_page(page_number)
|
|
770
|
+
return @page_manager.get_page(page_number) unless @buffer_pool
|
|
771
|
+
|
|
772
|
+
@buffer_pool.get_page(page_number).page
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
def write_page(page)
|
|
776
|
+
if @buffer_pool
|
|
777
|
+
@buffer_pool.write_page(page)
|
|
778
|
+
else
|
|
779
|
+
@page_manager.write_page(page)
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
# Read all records from a page with proper record header parsing
|
|
784
|
+
def read_page_records(page)
|
|
785
|
+
# Check cache first
|
|
786
|
+
cache_key = [page.page_number, page.header.data_end]
|
|
787
|
+
if @page_records_cache.key?(page.page_number)
|
|
788
|
+
cached = @page_records_cache[page.page_number]
|
|
789
|
+
if cached[:data_end] == page.header.data_end
|
|
790
|
+
@cache_hits += 1
|
|
791
|
+
return cached[:records]
|
|
792
|
+
end
|
|
793
|
+
end
|
|
794
|
+
@cache_misses += 1
|
|
795
|
+
|
|
796
|
+
records = []
|
|
797
|
+
header_size = PageHeader::SIZE
|
|
798
|
+
offset = header_size
|
|
799
|
+
|
|
800
|
+
while offset < page.header.data_end
|
|
801
|
+
begin
|
|
802
|
+
# Engine records use [record_id(8)] [data_length(4)]
|
|
803
|
+
# [flags(2)] [column_count(2)]. Keep this parser aligned with
|
|
804
|
+
# Engine#insert_row so compaction never treats a row id as a
|
|
805
|
+
# header-size field.
|
|
806
|
+
record_header = page.read(offset, 16)
|
|
807
|
+
|
|
808
|
+
if record_header && record_header.bytesize == 16
|
|
809
|
+
record_id, data_length, flags, = record_header.unpack("Q>L>S>S")
|
|
810
|
+
rec_header_size = 16
|
|
811
|
+
|
|
812
|
+
# Validate header
|
|
813
|
+
if rec_header_size >= 16 && data_length > 0 && data_length < (page.size - offset)
|
|
814
|
+
records << {
|
|
815
|
+
offset: offset,
|
|
816
|
+
length: data_length,
|
|
817
|
+
header_size: rec_header_size,
|
|
818
|
+
record_id: record_id,
|
|
819
|
+
flags: flags
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
offset += rec_header_size + data_length
|
|
823
|
+
else
|
|
824
|
+
# Invalid record - stop scanning
|
|
825
|
+
break
|
|
826
|
+
end
|
|
827
|
+
else
|
|
828
|
+
break
|
|
829
|
+
end
|
|
830
|
+
rescue => e
|
|
831
|
+
# Error reading record - stop
|
|
832
|
+
break
|
|
833
|
+
end
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
# Cache the result
|
|
837
|
+
if records.size <= @max_cache_size
|
|
838
|
+
@page_records_cache[page.page_number] = {
|
|
839
|
+
data_end: page.header.data_end,
|
|
840
|
+
records: records
|
|
841
|
+
}
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
records
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
# Calculate cache hit rate
|
|
848
|
+
def cache_hit_rate
|
|
849
|
+
total = @cache_hits + @cache_misses
|
|
850
|
+
return 0.0 if total == 0
|
|
851
|
+
(@cache_hits.to_f / total * 100).round(2)
|
|
852
|
+
end
|
|
853
|
+
end
|
|
854
|
+
end
|
|
855
|
+
end
|