rubydb 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile.lock +1 -1
- data/README.md +296 -227
- data/Rakefile +6 -1
- data/accelerator/bin/SHA256SUMS +6 -0
- data/accelerator/bin/rubydb-accelerator-darwin-amd64 +0 -0
- data/accelerator/bin/rubydb-accelerator-darwin-arm64 +0 -0
- data/accelerator/bin/rubydb-accelerator-linux-amd64 +0 -0
- data/accelerator/bin/rubydb-accelerator-linux-arm64 +0 -0
- data/accelerator/bin/rubydb-accelerator-windows-amd64.exe +0 -0
- data/accelerator/bin/rubydb-accelerator-windows-arm64.exe +0 -0
- data/accelerator/cmd/rubydb-accelerator/main.go +11 -0
- data/accelerator/go.mod +3 -0
- data/accelerator/internal/execution/aggregate.go +94 -0
- data/accelerator/internal/execution/distinct.go +22 -0
- data/accelerator/internal/execution/filter.go +73 -0
- data/accelerator/internal/execution/join.go +79 -0
- data/accelerator/internal/execution/operators.go +167 -0
- data/accelerator/internal/execution/scan.go +20 -0
- data/accelerator/internal/execution/sort.go +62 -0
- data/accelerator/internal/execution/types.go +136 -0
- data/accelerator/internal/execution/value.go +67 -0
- data/accelerator/internal/memory/arena.go +47 -0
- data/accelerator/internal/memory/reuse.go +22 -0
- data/accelerator/internal/metrics/registry.go +67 -0
- data/accelerator/internal/parallel/bounded_queue.go +56 -0
- data/accelerator/internal/parallel/scheduler.go +47 -0
- data/accelerator/internal/parallel/worker_pool.go +53 -0
- data/accelerator/internal/protocol/cancellation.go +48 -0
- data/accelerator/internal/protocol/columnar.go +263 -0
- data/accelerator/internal/protocol/frame.go +187 -0
- data/accelerator/internal/runtime/worker.go +521 -0
- data/accelerator/internal/storage/page_reader.go +81 -0
- data/accelerator/internal/storage/snapshot_scan.go +539 -0
- data/accelerator/internal/wal/checksum.go +13 -0
- data/accelerator/internal/wal/compression.go +41 -0
- data/accelerator/internal/wal/group_commit.go +24 -0
- data/accelerator/internal/wal/record_encoder.go +40 -0
- data/adapters/activerecord/README.md +8 -3
- data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +50 -42
- data/adapters/activerecord/rubydb-activerecord.gemspec +1 -1
- data/docs/README.md +3 -1
- data/docs/architecture/go-accelerator.md +179 -0
- data/docs/cli.md +24 -0
- data/docs/contributing/benchmarking.md +16 -0
- data/docs/developer/local-development.md +32 -0
- data/docs/release.md +2 -2
- data/lessons/02-local-development.md +2 -2
- data/lessons/04-rails-complex-apps.md +2 -2
- data/lessons/05-rubydb-production-server.md +2 -2
- data/lessons/07-hybrid-microservices.md +175 -90
- data/lessons/10-release-readiness.md +184 -117
- data/lessons/11-community-adapter.md +323 -0
- data/lessons/12-rails-ecommerce-pressure.md +263 -0
- data/lib/rubydb/accelerator/client.rb +451 -0
- data/lib/rubydb/accelerator/error.rb +22 -0
- data/lib/rubydb/accelerator/manager.rb +606 -0
- data/lib/rubydb/accelerator.rb +13 -0
- data/lib/rubydb/cli/application.rb +6 -1
- data/lib/rubydb/cli/commands/accelerator.rb +72 -0
- data/lib/rubydb/cli/commands/doctor.rb +3 -0
- data/lib/rubydb/client/client.rb +7 -0
- data/lib/rubydb/client/connection.rb +15 -0
- data/lib/rubydb/client/result.rb +5 -1
- data/lib/rubydb/configuration/defaults.rb +12 -0
- data/lib/rubydb/configuration/validation.rb +8 -1
- data/lib/rubydb/execution/accelerator_dispatch.rb +30 -0
- data/lib/rubydb/execution/cost_model.rb +72 -0
- data/lib/rubydb/execution/executor.rb +373 -11
- data/lib/rubydb/execution/operator_selection.rb +57 -0
- data/lib/rubydb/execution/physical_plan.rb +47 -0
- data/lib/rubydb/execution/planner.rb +12 -46
- data/lib/rubydb/execution/sort_executor.rb +22 -8
- data/lib/rubydb/indexes/btree.rb +31 -2
- data/lib/rubydb/rubydb.rb +7 -1
- data/lib/rubydb/server/session.rb +45 -0
- data/lib/rubydb/storage/engine.rb +74 -12
- data/lib/rubydb/storage/snapshot_reader.rb +167 -0
- data/lib/rubydb/version.rb +1 -1
- data/lib/rubydb/wal/archive.rb +17 -0
- data/lib/rubydb/wal/wal.rb +1 -0
- data/rubydb.gemspec +12 -2
- data/scripts/build_accelerator +49 -0
- data/scripts/release +34 -4
- data/scripts/replication_failover_drill +2 -2
- metadata +49 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# Lesson 11 — Build a RubyDB adapter for your community
|
|
2
|
+
|
|
3
|
+
This lesson is for a developer who wants to make RubyDB available to another
|
|
4
|
+
language or framework community. The goal is a real, supportable adapter that
|
|
5
|
+
can be released independently—not a thin helper that concatenates SQL.
|
|
6
|
+
|
|
7
|
+
RubyDB adapters connect to a running RubyDB server. They do not open `.rdb`
|
|
8
|
+
files directly. One Ruby process owns an embedded database path; a Python,
|
|
9
|
+
Node.js, Go, Java, Rust, or framework application should connect to RubyDB in
|
|
10
|
+
server mode.
|
|
11
|
+
|
|
12
|
+
## 1. Choose the community and the support boundary
|
|
13
|
+
|
|
14
|
+
Write this down before coding:
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
Adapter: rubydb-example
|
|
18
|
+
Language: Example 1.0+
|
|
19
|
+
Framework: Example Framework 4.x+
|
|
20
|
+
RubyDB: 0.1.x
|
|
21
|
+
Transport: rubydb:// for development, rubydbs:// for production
|
|
22
|
+
API: query, parameters, transactions, prepared statements, pooling
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Start with a narrow, honest support matrix. A language driver, an ORM adapter,
|
|
26
|
+
and a migration tool have different responsibilities:
|
|
27
|
+
|
|
28
|
+
- A language driver owns connections, parameters, results, errors, deadlines,
|
|
29
|
+
cancellation, transactions, TLS, and resource cleanup.
|
|
30
|
+
- An ORM adapter maps the ORM's query, type, transaction, schema, and pooling
|
|
31
|
+
APIs to the driver. It must not claim that RubyDB supports another database's
|
|
32
|
+
dialect just because the ORM has a familiar adapter name.
|
|
33
|
+
- A framework integration owns configuration, lifecycle hooks, health checks,
|
|
34
|
+
logging, and deployment conventions for that framework.
|
|
35
|
+
|
|
36
|
+
Do not promise PostgreSQL, MySQL, or SQLite compatibility unless the exact
|
|
37
|
+
syntax and behavior has been implemented and tested. Link users to RubyDB's
|
|
38
|
+
[SQL compatibility guide](../docs/sql/compatibility-guide.md) and state which
|
|
39
|
+
features your adapter supports.
|
|
40
|
+
|
|
41
|
+
## 2. Study the reference implementations
|
|
42
|
+
|
|
43
|
+
Use the repository's adapters as working references:
|
|
44
|
+
|
|
45
|
+
- [`adapters/python`](../adapters/python) demonstrates a DB-API 2.0 client.
|
|
46
|
+
- [`adapters/rubydb`](../adapters/rubydb) demonstrates a TypeScript client,
|
|
47
|
+
promises, TLS, pooling, prepared statements, and timeout cancellation.
|
|
48
|
+
- [`adapters/activerecord`](../adapters/activerecord) demonstrates a Ruby ORM
|
|
49
|
+
integration and Rails schema behavior.
|
|
50
|
+
|
|
51
|
+
Read the [wire protocol guide](../docs/server/protocol.md),
|
|
52
|
+
[`spec/wire/protocol.md`](../spec/wire/protocol.md), and
|
|
53
|
+
[`spec/protocol/protocol.md`](../spec/protocol/protocol.md) together. The
|
|
54
|
+
implementation and executable tests are authoritative when a draft document
|
|
55
|
+
does not describe a detail.
|
|
56
|
+
|
|
57
|
+
## 3. Create a maintainable package
|
|
58
|
+
|
|
59
|
+
Keep the adapter in its own repository or in `adapters/<community>` while it is
|
|
60
|
+
being developed. A useful layout is:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
rubydb-example/
|
|
64
|
+
├── README.md
|
|
65
|
+
├── LICENSE
|
|
66
|
+
├── CHANGELOG.md
|
|
67
|
+
├── CONTRIBUTING.md
|
|
68
|
+
├── SECURITY.md
|
|
69
|
+
├── package-or-project-manifest
|
|
70
|
+
├── src/
|
|
71
|
+
│ ├── connection
|
|
72
|
+
│ ├── protocol
|
|
73
|
+
│ ├── errors
|
|
74
|
+
│ ├── types
|
|
75
|
+
│ └── pool
|
|
76
|
+
├── tests/
|
|
77
|
+
│ ├── unit/
|
|
78
|
+
│ ├── protocol/
|
|
79
|
+
│ ├── integration/
|
|
80
|
+
│ └── security/
|
|
81
|
+
└── examples/
|
|
82
|
+
└── basic_app/
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Keep public API types separate from socket and JSON code. This makes it
|
|
86
|
+
possible to replace the transport or add a framework integration without
|
|
87
|
+
making application code depend on internal protocol objects.
|
|
88
|
+
|
|
89
|
+
Use a package name that is valid for the target registry and clearly belongs
|
|
90
|
+
to your maintainers. For npm, new package names must be lowercase; a scoped
|
|
91
|
+
package therefore looks like `@your-scope/rubydb`, not `@YourScope/rubydb`.
|
|
92
|
+
See npm's [package naming guidance](https://docs.npmjs.com/creating-a-package-json-file)
|
|
93
|
+
before reserving a name. Never use `rubydb` alone for an unofficial package.
|
|
94
|
+
|
|
95
|
+
## 4. Implement the protocol boundary
|
|
96
|
+
|
|
97
|
+
RubyDB uses bounded, newline-delimited JSON messages for its client/server
|
|
98
|
+
transport. Each message has an envelope with a `type`, an identifier, a
|
|
99
|
+
creation timestamp, and a payload. Keep the following invariants in the driver:
|
|
100
|
+
|
|
101
|
+
1. Parse the RubyDB URL and reject unknown schemes. Support `rubydb://` for a
|
|
102
|
+
trusted private network and `rubydbs://` for TLS.
|
|
103
|
+
2. Open one TCP or TLS connection and enable peer verification by default for
|
|
104
|
+
production TLS connections. Allow CA, client certificate, and key settings
|
|
105
|
+
through configuration or a secret manager, never through committed files.
|
|
106
|
+
3. Apply a maximum frame size before allocating unbounded memory. Reject an
|
|
107
|
+
oversized request or response and close the connection safely.
|
|
108
|
+
4. Send a handshake with the protocol version, client name, client version,
|
|
109
|
+
username, and database. Follow it with authentication and synchronization.
|
|
110
|
+
Fail closed when the server rejects any step.
|
|
111
|
+
5. Give every request a unique ID and correlate responses by ID. Do not assume
|
|
112
|
+
that response order will remain safe if multiplexing or asynchronous
|
|
113
|
+
notifications are added later.
|
|
114
|
+
6. Implement the supported operations: `query`, `prepare`, `execute`, `close`,
|
|
115
|
+
`begin`, `commit`, `rollback`, `ping`, and `terminate`.
|
|
116
|
+
7. Keep parameter values separate from SQL. Encode supported scalar, array,
|
|
117
|
+
object, date/time, and binary values according to the adapter's documented
|
|
118
|
+
mapping. Never interpolate user input into a query string.
|
|
119
|
+
8. Normalize result fields into the community's idioms while preserving column
|
|
120
|
+
metadata, rows, row count, affected rows, and insert identifiers.
|
|
121
|
+
9. Map server error codes into stable public exception types. Include a safe
|
|
122
|
+
message and code, but do not expose passwords, TLS keys, or raw secrets in
|
|
123
|
+
logs.
|
|
124
|
+
|
|
125
|
+
A driver request should conceptually look like this; use the target language's
|
|
126
|
+
JSON and socket APIs rather than copying this pseudocode literally:
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
request_id = new_unique_id()
|
|
130
|
+
send {
|
|
131
|
+
type: "query",
|
|
132
|
+
id: request_id,
|
|
133
|
+
created_at: now_as_iso8601,
|
|
134
|
+
payload: {
|
|
135
|
+
sql: "SELECT id, name FROM users WHERE active = ?",
|
|
136
|
+
params: [true],
|
|
137
|
+
deadline_at: deadline_as_iso8601
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
response = read_and_match_id(request_id)
|
|
141
|
+
return normalize_result(response.payload.result || response.payload.data)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The exact wire behavior belongs in protocol tests, not in assumptions hidden in
|
|
145
|
+
the adapter. If you need a protocol capability that RubyDB does not advertise,
|
|
146
|
+
open a design issue before inventing a private message type.
|
|
147
|
+
|
|
148
|
+
## 5. Make transaction behavior explicit
|
|
149
|
+
|
|
150
|
+
Expose explicit `begin`, `commit`, and `rollback` operations. If your
|
|
151
|
+
community API has implicit transactions, document exactly when they begin and
|
|
152
|
+
how a connection returns to an idle state.
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
connection.begin()
|
|
156
|
+
try:
|
|
157
|
+
connection.execute(
|
|
158
|
+
"INSERT INTO events (name) VALUES (?)",
|
|
159
|
+
["community-adapter.started"]
|
|
160
|
+
)
|
|
161
|
+
connection.commit()
|
|
162
|
+
except:
|
|
163
|
+
connection.rollback()
|
|
164
|
+
raise
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
A connection must not be returned to a pool while it has an open transaction,
|
|
168
|
+
an active cursor, or an unclosed prepared statement. On network loss, mark the
|
|
169
|
+
connection unusable and roll back locally; do not silently reuse it.
|
|
170
|
+
|
|
171
|
+
## 6. Implement deadlines and cancellation safely
|
|
172
|
+
|
|
173
|
+
Every potentially long operation needs a bounded timeout. On timeout, send a
|
|
174
|
+
wire `cancel` request containing the timed-out request's ID, then drain or
|
|
175
|
+
close the connection according to the response. Cancellation is cooperative;
|
|
176
|
+
it is not permission to kill a thread while it owns database state.
|
|
177
|
+
|
|
178
|
+
Never automatically retry a write just because the client timed out. The write
|
|
179
|
+
may have committed before the response was lost. Tell users to use an
|
|
180
|
+
idempotency key or application-level deduplication for retryable writes.
|
|
181
|
+
|
|
182
|
+
Test all of these cases:
|
|
183
|
+
|
|
184
|
+
- timeout before the server starts execution;
|
|
185
|
+
- cancellation during a long-running query;
|
|
186
|
+
- a cancellation response for an unknown request;
|
|
187
|
+
- connection loss while cancellation is being sent; and
|
|
188
|
+
- a late response arriving after the caller has timed out.
|
|
189
|
+
|
|
190
|
+
## 7. Add pooling without hiding failures
|
|
191
|
+
|
|
192
|
+
Provide a bounded pool only if the target community expects one. The pool must
|
|
193
|
+
have a maximum size, acquisition timeout, idle cleanup, connection validation,
|
|
194
|
+
and deterministic shutdown. A checkout/return API should make ownership clear:
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
pool = Pool(url, min_size=1, max_size=8)
|
|
198
|
+
try:
|
|
199
|
+
rows = pool.use(lambda db:
|
|
200
|
+
db.query("SELECT id FROM jobs WHERE state = ?", ["ready"]).rows
|
|
201
|
+
)
|
|
202
|
+
finally:
|
|
203
|
+
pool.close()
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Do not create one unbounded connection per request. Do not share one connection
|
|
207
|
+
between concurrent operations unless the API and protocol explicitly support
|
|
208
|
+
that behavior. Pool limits should be lower than the server's connection and
|
|
209
|
+
resource limits, with headroom for health checks and migrations.
|
|
210
|
+
|
|
211
|
+
## 8. Test the adapter against a real server
|
|
212
|
+
|
|
213
|
+
A protocol fixture is useful for fast unit tests, but it cannot prove that the
|
|
214
|
+
adapter works. Add a live integration job that starts a pinned RubyDB server
|
|
215
|
+
and runs the adapter against a temporary database.
|
|
216
|
+
|
|
217
|
+
Minimum test groups:
|
|
218
|
+
|
|
219
|
+
- URL parsing, defaults, TLS options, type conversion, and public errors;
|
|
220
|
+
- fragmented frames, multiple frames, blank lines, malformed JSON, and
|
|
221
|
+
oversized frames;
|
|
222
|
+
- handshake, authentication failure, authorization failure, ping, and close;
|
|
223
|
+
- parameterized CRUD, `NULL`, booleans, numbers, dates, text, arrays, and JSON;
|
|
224
|
+
- prepared statements and statement cleanup;
|
|
225
|
+
- commit, rollback, transaction isolation expectations, and pool reuse;
|
|
226
|
+
- deadline, wire cancellation, late responses, and connection loss;
|
|
227
|
+
- concurrent operations up to the documented pool limit;
|
|
228
|
+
- server restart, backup/restore validation, and version mismatch behavior; and
|
|
229
|
+
- secrets absent from exceptions, logs, test output, and published artifacts.
|
|
230
|
+
|
|
231
|
+
Run the RubyDB repository checks first:
|
|
232
|
+
|
|
233
|
+
```powershell
|
|
234
|
+
bundle install
|
|
235
|
+
bundle exec rspec
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Then run the adapter's fast and live suites. The exact commands depend on the
|
|
239
|
+
language, but the live suite should receive a URL from the environment rather
|
|
240
|
+
than hard-code credentials:
|
|
241
|
+
|
|
242
|
+
```powershell
|
|
243
|
+
$env:RUBYDB_URL = "rubydb://rubydb@127.0.0.1:7432/rubydb"
|
|
244
|
+
your-package-test-command
|
|
245
|
+
your-package-live-integration-command
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Repeat the live suite over `rubydbs://` with a test CA. Add property or fuzz
|
|
249
|
+
tests for the frame decoder and parameter encoder. A driver that passes only a
|
|
250
|
+
mock server test is not ready for a community release.
|
|
251
|
+
|
|
252
|
+
## 9. Document production usage
|
|
253
|
+
|
|
254
|
+
Your README should include copy-and-paste examples for:
|
|
255
|
+
|
|
256
|
+
1. local development with an isolated database;
|
|
257
|
+
2. starting RubyDB server mode;
|
|
258
|
+
3. setting `RUBYDB_URL` through the platform's secret store;
|
|
259
|
+
4. TLS with peer verification and certificate rotation;
|
|
260
|
+
5. pool sizing and request timeouts;
|
|
261
|
+
6. migrations and backup/restore procedures;
|
|
262
|
+
7. health checks and metrics; and
|
|
263
|
+
8. unsupported SQL, RubyDB versions, operating systems, and framework versions.
|
|
264
|
+
|
|
265
|
+
Show users the production shape:
|
|
266
|
+
|
|
267
|
+
```text
|
|
268
|
+
Application processes ──TLS/private network──> RubyDB server
|
|
269
|
+
secrets from manager durable storage + backups
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The adapter is not the database server, a backup system, or a failover
|
|
273
|
+
controller. Link to RubyDB's [production operations guide](../docs/operations/production-guide.md)
|
|
274
|
+
and require users to validate their own workload before making availability or
|
|
275
|
+
durability claims.
|
|
276
|
+
|
|
277
|
+
## 10. Release and maintain the community package
|
|
278
|
+
|
|
279
|
+
Before the first release:
|
|
280
|
+
|
|
281
|
+
- choose a license and add a changelog;
|
|
282
|
+
- publish a support matrix and compatibility policy;
|
|
283
|
+
- enable CI on every supported language/runtime version;
|
|
284
|
+
- run unit, live, security, fuzz, and package-content checks;
|
|
285
|
+
- verify the package contains no `.env`, credentials, private keys, databases,
|
|
286
|
+
build caches, or test secrets;
|
|
287
|
+
- use trusted publishing or a short-lived registry token in CI;
|
|
288
|
+
- sign releases when the target registry supports signing;
|
|
289
|
+
- tag the source commit and record the RubyDB protocol/server version; and
|
|
290
|
+
- provide a security contact and a responsible disclosure policy.
|
|
291
|
+
|
|
292
|
+
For an npm package, inspect the tarball before publishing:
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
npm test
|
|
296
|
+
npm pack --dry-run
|
|
297
|
+
npm publish --access public
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
For Python, Ruby, Rust, or another registry, use that ecosystem's equivalent
|
|
301
|
+
build, metadata, signature, and upload checks. Release the adapter separately
|
|
302
|
+
from RubyDB and pin compatible versions in the package metadata. After release,
|
|
303
|
+
install the package in a clean environment and rerun the live smoke test.
|
|
304
|
+
|
|
305
|
+
## Community contribution checklist
|
|
306
|
+
|
|
307
|
+
Open a pull request or design issue with:
|
|
308
|
+
|
|
309
|
+
```text
|
|
310
|
+
[ ] Adapter name, owner, license, and supported versions are listed.
|
|
311
|
+
[ ] Server-mode boundary and embedded-mode limitation are documented.
|
|
312
|
+
[ ] Parameter binding is used for every user value.
|
|
313
|
+
[ ] Frame limits, malformed input, TLS verification, and secret handling exist.
|
|
314
|
+
[ ] Transactions, timeouts, cancellation, and connection cleanup are tested.
|
|
315
|
+
[ ] Live tests pass against a pinned RubyDB server.
|
|
316
|
+
[ ] Package contents and release provenance are checked.
|
|
317
|
+
[ ] README includes installation, examples, operations, and limitations.
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The adapter becomes part of the wider RubyDB ecosystem when users can install
|
|
321
|
+
it, understand its limits, run a real query, observe failures, and upgrade it
|
|
322
|
+
without guessing. That standard protects both RubyDB users and the community
|
|
323
|
+
maintainer.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Lesson 12: Build and pressure-test a Rails shop with RubyDB
|
|
2
|
+
|
|
3
|
+
This lesson uses the runnable application in
|
|
4
|
+
[`examples/rails_ecommerce`](../examples/rails_ecommerce/). It is intentionally
|
|
5
|
+
small enough to understand, but it exercises the database paths that usually
|
|
6
|
+
matter in a commerce service:
|
|
7
|
+
|
|
8
|
+
- catalog filters, ordering, limits, and compound indexes;
|
|
9
|
+
- grouped order summaries;
|
|
10
|
+
- customer/order/item associations and eager loading;
|
|
11
|
+
- a transaction that creates an order and updates inventory;
|
|
12
|
+
- direct ActiveRecord pressure and HTTP request pressure.
|
|
13
|
+
|
|
14
|
+
The example is a validation tool. A successful local run proves that this
|
|
15
|
+
specific application and workload work together; it is not a capacity promise
|
|
16
|
+
for every Rails application or deployment.
|
|
17
|
+
|
|
18
|
+
## 1. Copy the example and install it
|
|
19
|
+
|
|
20
|
+
From a checkout of RubyDB:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
cd examples/rails_ecommerce
|
|
24
|
+
bundle install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The example uses local paths to the RubyDB engine and ActiveRecord adapter, so
|
|
28
|
+
you can test the code currently checked out without publishing a new gem.
|
|
29
|
+
When using released gems in your own application, use pinned versions instead:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
gem "rubydb", "0.1.6"
|
|
33
|
+
gem "rubydb-activerecord", "0.1.3"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 2. Create and seed the local database
|
|
37
|
+
|
|
38
|
+
Embedded mode is the easiest local development setup. RubyDB creates the file
|
|
39
|
+
under `examples/rails_ecommerce/tmp/` and Rails talks to it through the real
|
|
40
|
+
ActiveRecord adapter. The example pins embedded Puma and the ActiveRecord pool
|
|
41
|
+
to one thread/connection because one embedded RubyDB path has one process-local
|
|
42
|
+
owner:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
bundle exec rails db:prepare
|
|
46
|
+
RUBYDB_PRODUCTS=500 RUBYDB_CUSTOMERS=100 RUBYDB_ORDERS=1000 bundle exec rails db:seed
|
|
47
|
+
bundle exec ruby script/smoke.rb
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
PowerShell:
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
$env:RUBYDB_PRODUCTS = "500"
|
|
54
|
+
$env:RUBYDB_CUSTOMERS = "100"
|
|
55
|
+
$env:RUBYDB_ORDERS = "1000"
|
|
56
|
+
bundle exec rails db:prepare
|
|
57
|
+
bundle exec rails db:seed
|
|
58
|
+
bundle exec ruby script/smoke.rb
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The seed is deterministic. Change the three environment variables to create a
|
|
62
|
+
larger fixture without changing the application:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
RUBYDB_PRODUCTS=10000 RUBYDB_CUSTOMERS=2000 RUBYDB_ORDERS=25000 bundle exec rails db:seed
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 3. Understand the Rails queries
|
|
69
|
+
|
|
70
|
+
The catalog action uses a filtered and ordered relation:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
Product.active
|
|
74
|
+
.in_category(params[:category])
|
|
75
|
+
.order(price_cents: :asc)
|
|
76
|
+
.limit(50)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
It also executes a grouped aggregate for the category navigation:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
Product.active.group(:category).count
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The smoke test exercises eager loading and a grouped order query:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
Order.completed.group(:status).count
|
|
89
|
+
Order.includes(:customer, :order_items).order(id: :desc).first
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The order action keeps the business write atomic:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
Order.transaction do
|
|
96
|
+
order = customer.orders.create!(status: "paid", total_cents: total)
|
|
97
|
+
order.order_items.create!(product: product, quantity: quantity, unit_price_cents: price)
|
|
98
|
+
product.update!(stock: product.stock - quantity)
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
In a real store, add an explicit inventory reservation strategy, idempotency
|
|
103
|
+
keys, payment authorization boundaries, audit records, and a concurrency test
|
|
104
|
+
for overselling. The small example keeps the business flow visible for
|
|
105
|
+
learning.
|
|
106
|
+
|
|
107
|
+
## 4. Run direct database pressure
|
|
108
|
+
|
|
109
|
+
Run the database workload without HTTP overhead:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
RUBYDB_PRESSURE_OPERATIONS=1000 bundle exec ruby script/pressure.rb
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The output is JSON containing completed operations, errors, throughput, and
|
|
116
|
+
p50/p95/p99 latency. The workload randomly exercises catalog reads, a `LIKE`
|
|
117
|
+
filter, grouped order counts, and eager-loaded order history.
|
|
118
|
+
|
|
119
|
+
To add transaction writes:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
RUBYDB_PRESSURE_MODE=mixed \
|
|
123
|
+
RUBYDB_PRESSURE_WRITE_RATIO=0.10 \
|
|
124
|
+
RUBYDB_PRESSURE_OPERATIONS=1000 \
|
|
125
|
+
bundle exec ruby script/pressure.rb
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The script exits non-zero when any operation fails. Treat the first error as a
|
|
129
|
+
correctness issue to investigate, not as an acceptable benchmark result.
|
|
130
|
+
|
|
131
|
+
## 5. Run the Rails app and HTTP pressure
|
|
132
|
+
|
|
133
|
+
Start the app:
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
bundle exec rails server -b 127.0.0.1 -p 3002
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
In a second terminal, send concurrent requests to the JSON catalog endpoint:
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
RUBYDB_HTTP_THREADS=8 \
|
|
143
|
+
RUBYDB_HTTP_REQUESTS=500 \
|
|
144
|
+
bundle exec ruby script/http_pressure.rb
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
PowerShell:
|
|
148
|
+
|
|
149
|
+
```powershell
|
|
150
|
+
$env:RUBYDB_HTTP_THREADS = "8"
|
|
151
|
+
$env:RUBYDB_HTTP_REQUESTS = "500"
|
|
152
|
+
bundle exec ruby script/http_pressure.rb
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This measures Rails routing, controller work, serialization, and database
|
|
156
|
+
reads together. It does not replace a load test that runs from another host,
|
|
157
|
+
but it gives a reproducible local baseline.
|
|
158
|
+
|
|
159
|
+
## 6. Test the multi-process topology
|
|
160
|
+
|
|
161
|
+
Embedded mode is for one process owning one database path. For concurrent web
|
|
162
|
+
traffic, web workers,
|
|
163
|
+
job workers, or more than one host, run a RubyDB server and connect over the
|
|
164
|
+
protocol. From the example directory, start the local server using the
|
|
165
|
+
repository executable:
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
mkdir -p tmp/rubydb-commerce-server
|
|
169
|
+
bundle exec ruby ../../exe/rubydb start \
|
|
170
|
+
--host 127.0.0.1 \
|
|
171
|
+
--port 7432 \
|
|
172
|
+
--data-dir tmp/rubydb-commerce-server \
|
|
173
|
+
--log-dir tmp/rubydb-commerce-server/log
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
In another terminal, migrate and seed through the server:
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
RUBYDB_EMBEDDED=false \
|
|
180
|
+
RUBYDB_URL='rubydb://rubydb@127.0.0.1:7432/rubydb' \
|
|
181
|
+
bundle exec rails db:prepare
|
|
182
|
+
|
|
183
|
+
RUBYDB_EMBEDDED=false \
|
|
184
|
+
RUBYDB_URL='rubydb://rubydb@127.0.0.1:7432/rubydb' \
|
|
185
|
+
RUBYDB_PRODUCTS=1000 RUBYDB_CUSTOMERS=200 RUBYDB_ORDERS=5000 \
|
|
186
|
+
bundle exec rails db:seed
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Then run several Rails database workers against the server:
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
RUBYDB_EMBEDDED=false \
|
|
193
|
+
RUBYDB_URL='rubydb://rubydb@127.0.0.1:7432/rubydb' \
|
|
194
|
+
RAILS_MAX_THREADS=8 \
|
|
195
|
+
RUBYDB_PRESSURE_THREADS=8 \
|
|
196
|
+
RUBYDB_PRESSURE_OPERATIONS=2000 \
|
|
197
|
+
bundle exec ruby script/pressure.rb
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
For TLS, use a `rubydbs://` URL and configure certificate verification. In a
|
|
201
|
+
real deployment, inject the URL through a secret manager; do not commit a
|
|
202
|
+
password into `database.yml` or a benchmark script.
|
|
203
|
+
|
|
204
|
+
## 7. Read the results correctly
|
|
205
|
+
|
|
206
|
+
Record the JSON output with the Git commit and fixture size. Compare:
|
|
207
|
+
|
|
208
|
+
1. p50 for normal user requests;
|
|
209
|
+
2. p95 and p99 for tail latency under pressure;
|
|
210
|
+
3. throughput and error count;
|
|
211
|
+
4. server CPU, memory, WAL growth, disk space, and rejected connections;
|
|
212
|
+
5. results before and after enabling an accelerator policy.
|
|
213
|
+
|
|
214
|
+
Do not compare one tiny in-memory run with a production claim. Go acceleration
|
|
215
|
+
is adaptive: small queries can stay in Ruby when process/protocol overhead is
|
|
216
|
+
larger than the work, while eligible large immutable-snapshot operations can be
|
|
217
|
+
accelerated. Correct results, durable commits, and bounded resource use come
|
|
218
|
+
before a lower benchmark number.
|
|
219
|
+
|
|
220
|
+
## 8. Run the accelerator A/B check
|
|
221
|
+
|
|
222
|
+
From the repository root, run the same 10,000-row workload with a Ruby-only
|
|
223
|
+
baseline and the required Go worker:
|
|
224
|
+
|
|
225
|
+
```powershell
|
|
226
|
+
$env:RUBYDB_ACCELERATOR_ROWS = "10000"
|
|
227
|
+
$env:RUBYDB_ACCELERATOR_ITERATIONS = "5"
|
|
228
|
+
$env:RUBYDB_ACCELERATOR_THREADS = "4"
|
|
229
|
+
$env:RUBYDB_ACCELERATOR_REQUESTS = "5"
|
|
230
|
+
bundle exec ruby benchmarks/go_accelerator.rb
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The benchmark verifies the row result before and during measurement, performs
|
|
234
|
+
an explicit worker restart, and reports p50/p95/p99 latency, throughput, CPU,
|
|
235
|
+
RSS, concurrent-request errors, worker starts/restarts/failures, and a speed
|
|
236
|
+
gate. `performance_gate_passed` must be `true` for this exact workload and
|
|
237
|
+
machine before selecting `RUBYDB_ACCELERATOR=required`; otherwise leave the
|
|
238
|
+
policy at `auto` or `off`. A Go worker can be correct but slower when the
|
|
239
|
+
workload is dominated by copying Ruby objects through the process boundary.
|
|
240
|
+
|
|
241
|
+
For larger Rails pressure, use at least 10,000 products and 10,000 orders,
|
|
242
|
+
then run the direct and server-mode pressure commands above. Include the
|
|
243
|
+
result JSON, commit, Ruby/Rails versions, and host metrics in the performance
|
|
244
|
+
record.
|
|
245
|
+
|
|
246
|
+
## 9. Production checklist for this app
|
|
247
|
+
|
|
248
|
+
Before using the pattern for a real service:
|
|
249
|
+
|
|
250
|
+
- pin Ruby, Rails, RubyDB, and adapter versions;
|
|
251
|
+
- use server mode for multiple processes and hosts;
|
|
252
|
+
- run migrations against a backup or restored staging copy first;
|
|
253
|
+
- configure TLS, authentication, connection limits, timeouts, and monitoring;
|
|
254
|
+
- test idempotent order creation and payment retries;
|
|
255
|
+
- test inventory contention and deadlock/timeout behavior;
|
|
256
|
+
- run backup, restore, restart, and disk-space drills;
|
|
257
|
+
- establish an application-specific p95/p99 SLO with representative data;
|
|
258
|
+
- keep PostgreSQL as the comparison target when the application needs its
|
|
259
|
+
broader SQL dialect, ecosystem, or large-scale operational guarantees.
|
|
260
|
+
|
|
261
|
+
This lesson makes RubyDB easy to try locally while keeping the boundary clear:
|
|
262
|
+
the benchmark measures the features the example actually uses, and production
|
|
263
|
+
readiness still requires validation of the exact application and topology.
|