trellis-pg 0.1.1 → 0.2.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 +4 -4
- data/README.md +88 -43
- data/ext/trellis_ruby/Cargo.lock +5 -6
- data/ext/trellis_ruby/Cargo.toml +3 -3
- data/ext/trellis_ruby/src/lib.rs +215 -8
- data/lib/trellis/error.rb +27 -3
- data/lib/trellis/instance.rb +31 -1
- data/lib/trellis/pg.rb +2 -1
- data/lib/trellis/schema_dump.rb +210 -66
- data/lib/trellis/values.rb +165 -3
- data/lib/trellis/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 989a2bb8e00efa189fc95684dca175ecbdf17e0e6227ea26462c837276418567
|
|
4
|
+
data.tar.gz: 70d484a63fece2c1e4c61821f2565b3431638e65b74b6ad724fd22396c5df6bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a0dde1a7bc9e39fdf24a27fe6130591d0423ae1f89ff059b61d0d06b65f186686486b18552e448ba46c4bd8427a6043e9365022feef9e637cd54166904a4996c
|
|
7
|
+
data.tar.gz: 05b66f0c6d95a08173f2805d1b75cc2bbb00b53c484e62d3d3ec4f2a16a4b572c7f2320b5a2d04addd4903aa0d8708dfdbdf9da7325edae1959eb39f7553d06f
|
data/README.md
CHANGED
|
@@ -18,7 +18,7 @@ as `trellis/pg`. The module is still `Trellis`.
|
|
|
18
18
|
|
|
19
19
|
```ruby
|
|
20
20
|
# Gemfile
|
|
21
|
-
gem "trellis-pg", "~> 0.
|
|
21
|
+
gem "trellis-pg", "~> 0.2.0"
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
`Bundler.require`, which a Rails app's `config/application.rb` runs,
|
|
@@ -78,10 +78,38 @@ Nothing in the binding uses it.
|
|
|
78
78
|
`table`, `columns`, `role` (`:group_by`, `:join_from` or `:join_joined`: the part of
|
|
79
79
|
the definition that needs it), `opted_out` and a `create index` statement as `fix`);
|
|
80
80
|
`self_check` refuses such a target unless it was defined `WITHOUT SOURCE
|
|
81
|
-
INDEX`. `status` lists the
|
|
81
|
+
INDEX`. `status` lists the source indexes a `MIN`/`MAX` aggregate's probes
|
|
82
|
+
read and the source lacks (`extreme_index_gaps`, each a
|
|
83
|
+
`Trellis::ExtremeIndexGap` with `table`, `columns`, `argument`, `opted_out`
|
|
84
|
+
and `fix`); nothing pauses, and a deleted extreme is re-read by scanning
|
|
85
|
+
its group. `status` lists the partitions of a partitioned source a definition
|
|
82
86
|
is being corrected for while it reads `:backfilling` for them
|
|
83
87
|
(`correcting`, each a `Trellis::CorrectingNode` with `node`, `since` and
|
|
84
|
-
`keys_remaining`).
|
|
88
|
+
`keys_remaining`). `status` names what a Re-derive build waits on instead
|
|
89
|
+
of its own work (`build_wait_xid`, the transaction id of the fence it
|
|
90
|
+
waits out, `nil` when it waits on nothing). While a process of another
|
|
91
|
+
Trellis version is live, `status` reports that this process stands by
|
|
92
|
+
(`version_handover`, a `Trellis::VersionHandover` with `this_version`,
|
|
93
|
+
`processes`, each a `Trellis::OtherProcess` with `worker_id` and
|
|
94
|
+
`version` (`nil` for a release before 0.2.0),
|
|
95
|
+
`unregistered_staging_worker` and `migrations_pending`), and
|
|
96
|
+
state-changing calls raise a `version_handover` error until it is gone.
|
|
97
|
+
While this binary is below the catalog floor, `status` reports that this
|
|
98
|
+
process stands by (`below_catalog_floor`, a `Trellis::BelowCatalogFloor`
|
|
99
|
+
with `floor` and `this_version`), and state-changing calls raise a
|
|
100
|
+
`version_floor` error.
|
|
101
|
+
For a `JOIN` definition `status` reports its hidden tables (`join`, a
|
|
102
|
+
`Trellis::JoinStatus`): `hidden_tables`, each a `Trellis::HiddenTable` with
|
|
103
|
+
`table`, `name`, `estimated_rows` and `bytes`, the `P`-size warning
|
|
104
|
+
(`partials_warning`, a `Trellis::PartialsWarning`), a 1-1 `JOIN`'s fan-out
|
|
105
|
+
warnings (`fan_out_warnings`, one `Trellis::FanOutWarning` per joined
|
|
106
|
+
position past the threshold, naming its `position`) and the step its build
|
|
107
|
+
runs (`building`, a `Trellis::JoinBuildStep` with `step` and the `side`,
|
|
108
|
+
`part`, `leaf`, `remaining` or `position` its kind of step carries). `join` is
|
|
109
|
+
`nil` for any other definition. `status` reports the oldest Trellis release
|
|
110
|
+
that can run a definition (`floor`, `"major.minor.patch"`), which an
|
|
111
|
+
application reads before rolling back to an earlier Trellis release
|
|
112
|
+
([upgrading](../../docs/upgrading.md#rolling-back)).
|
|
85
113
|
- `Trellis.request_backfill(source_table)`,
|
|
86
114
|
`Trellis.detect_capture_disruptions` (pauses now the readers of every table
|
|
87
115
|
whose Trellis capture was changed outside Trellis, and returns the tables),
|
|
@@ -161,7 +189,7 @@ one stand by; that is harmless, and each polls the lock every 5 seconds (see
|
|
|
161
189
|
At the defaults, a handle holds up to 20 Postgres connections (its call pool,
|
|
162
190
|
opened as calls need them and closed after 60 seconds idle) and 3 threads. A
|
|
163
191
|
worker handle (`staging: true, drain_threads: N`) also owns a background pool
|
|
164
|
-
of up to `2N +
|
|
192
|
+
of up to `2N + 6` connections (`2N + 5` without staging, `2` with staging
|
|
165
193
|
alone), closed after 60 seconds idle, and two connections that stay open: the
|
|
166
194
|
staging session and one `LISTEN` shared by its drain threads. It holds 6
|
|
167
195
|
threads: drain threads are tasks on its runtime, not OS threads. Its memory is
|
|
@@ -262,7 +290,9 @@ default one included.
|
|
|
262
290
|
```
|
|
263
291
|
- **Errors** are raised as a subclass of `Trellis::Error` per error code
|
|
264
292
|
(`ParseError`, `ValidationError`, `ConnectivityError`, `ConflictError`,
|
|
265
|
-
`NotFoundError`, `InternalError`, `TimeoutError
|
|
293
|
+
`NotFoundError`, `InternalError`, `TimeoutError`, `VersionHandoverError`,
|
|
294
|
+
`VersionFloorError`),
|
|
295
|
+
each with `#code` (a
|
|
266
296
|
symbol) and `#message`. A code newer than the binding arrives as
|
|
267
297
|
`UnknownError`, naming the code in its message. `ForkedHandleError` is a
|
|
268
298
|
`ValidationError`. Raising is Ruby's idiom, so where the Elixir binding
|
|
@@ -493,9 +523,8 @@ true, drain_threads: 1`), because the resume rebuilds through them.
|
|
|
493
523
|
A Rails app that uses Trellis dumps its schema as `structure.sql`
|
|
494
524
|
(`config.active_record.schema_format = :sql`), and the dump carries Trellis's
|
|
495
525
|
definitions, the way it carries `schema_migrations`. `schema.rb` can't carry
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
writes nothing.
|
|
526
|
+
them, so with `schema_format = :ruby` the dump (`db:migrate`'s and
|
|
527
|
+
`db:schema:dump`) raises `Trellis::ValidationError` and writes nothing.
|
|
499
528
|
|
|
500
529
|
```ruby
|
|
501
530
|
# config/application.rb
|
|
@@ -503,49 +532,65 @@ config.active_record.schema_format = :sql
|
|
|
503
532
|
config.trellis.connect = { url: ENV.fetch("DATABASE_URL") }
|
|
504
533
|
```
|
|
505
534
|
|
|
506
|
-
Nothing else is set up.
|
|
507
|
-
|
|
508
|
-
`
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
`
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
535
|
+
Nothing else is set up. The file has the application's tables, the target
|
|
536
|
+
tables (with their indexes and grants), and one row per definition: its
|
|
537
|
+
source, target and `JOIN` table, its text as written, its grammar version
|
|
538
|
+
and its floor. Trellis's own schema, the capture triggers on source tables
|
|
539
|
+
and the 1-1 targets' ledgers are not in it, so regenerating the dump changes
|
|
540
|
+
it only where a definition changed. `db:schema:load`, `db:prepare`,
|
|
541
|
+
`db:setup` and the test database `maintain_test_schema!` loads then run
|
|
542
|
+
`migrate` for `config.trellis.connect` and each of `config.trellis.instances`,
|
|
543
|
+
which creates Trellis's tables and restores each definition into its dumped
|
|
544
|
+
target. Each is restored unbuilt, and the worker builds it from the source
|
|
545
|
+
rows; a test database has none, so that is immediate. A file loaded another
|
|
546
|
+
way (`psql -f structure.sql`) needs `bin/rails trellis:migrate` after it.
|
|
547
|
+
How the restore works, and what it refuses, is in
|
|
548
|
+
[`docs/embedding.md`](../../docs/embedding.md#schema-dumps).
|
|
549
|
+
|
|
550
|
+
- The hooks apply to one database config, `config.trellis.dump_database`
|
|
521
551
|
(default `"primary"`): the one `config.trellis.connect` and
|
|
522
|
-
`config.trellis.instances` point at. Other databases dump as Rails
|
|
523
|
-
|
|
524
|
-
`config.trellis.connect` or an instance points at a
|
|
525
|
-
that config. They are compared by host, port and
|
|
526
|
-
config's as libpq resolves them (`PGHOST`,
|
|
527
|
-
included), Trellis's `url` from the url alone
|
|
528
|
-
user's name). A config with no host uses libpq's
|
|
529
|
-
matches a local Trellis host (`localhost`,
|
|
530
|
-
directory). Other host spellings are not
|
|
531
|
-
`127.0.0.1`.
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
552
|
+
`config.trellis.instances` point at. Other databases dump and load as Rails
|
|
553
|
+
does it. The dump and the load raise `Trellis::ValidationError`, the dump
|
|
554
|
+
writing nothing, when `config.trellis.connect` or an instance points at a
|
|
555
|
+
different database than that config. They are compared by host, port and
|
|
556
|
+
database name: the Rails config's as libpq resolves them (`PGHOST`,
|
|
557
|
+
`PGPORT` and `PGDATABASE` included), Trellis's `url` from the url alone
|
|
558
|
+
(port 5432, database the user's name). A config with no host uses libpq's
|
|
559
|
+
local socket, which matches a local Trellis host (`localhost`,
|
|
560
|
+
`127.0.0.1`, `::1` or a socket directory). Other host spellings are not
|
|
561
|
+
equated, so `localhost` is not `127.0.0.1`.
|
|
562
|
+
- `db:prepare` in development also dumps and loads the test database, and
|
|
563
|
+
`db:test:prepare` loads it. That dump is checked against development's
|
|
564
|
+
config of the same name, which `config.trellis.connect` points at, and the
|
|
565
|
+
load's restore connects to the test database: the configured options with
|
|
566
|
+
the test config's database, and its host, port, user, password and other
|
|
567
|
+
libpq keywords (`sslmode`, `sslrootcert`, ...) where it names a non-blank
|
|
568
|
+
value (its `url` when it has one).
|
|
569
|
+
- The hook adds each instance's exclusions (its Trellis schema, its ledgers)
|
|
570
|
+
to the flags Rails passes `pg_dump`, and its `target_schema` to the
|
|
571
|
+
`--schema` flags when Rails passes any. `pg_dump` can't leave out one
|
|
572
|
+
trigger, so the hook takes the capture triggers out of its output
|
|
573
|
+
(`Trellis::Instance#filter_schema_dump`).
|
|
574
|
+
- `pg_dump` and `Trellis::Instance#schema_dump` read separate snapshots. The
|
|
575
|
+
hook reads the definitions before and after the `pg_dump` and redoes the
|
|
576
|
+
dump when they differ (three attempts, then `Trellis::ConflictError`), so a
|
|
577
|
+
define or drop that races a dump can't leave tables and definitions
|
|
578
|
+
disagreeing. A dump that fails once `pg_dump` has written `structure.sql`
|
|
579
|
+
removes it, since what Rails wrote has no definitions and would load
|
|
580
|
+
without a word; a file no `pg_dump` wrote to is left as it was.
|
|
538
581
|
- A definition that was paused or quarantined where the dump was taken loads
|
|
539
|
-
|
|
582
|
+
unpaused: the pause is runtime state.
|
|
583
|
+
- A `structure.sql` written by an earlier release, which carries Trellis's
|
|
584
|
+
schema and its definitions' catalog rows, still loads as it did.
|
|
540
585
|
- `pg_dump` 17 writes `CREATE SCHEMA public`, which a database made by
|
|
541
586
|
`db:create` already has; the hook writes `CREATE SCHEMA IF NOT EXISTS
|
|
542
587
|
public` instead.
|
|
543
588
|
- The dump connects through `Trellis::Railtie.with_handle`, so it migrates
|
|
544
589
|
Trellis's tables first if the database lacks them, and starts no worker.
|
|
545
590
|
- It hooks `ActiveRecord::Tasks::DatabaseTasks.dump_schema` and
|
|
546
|
-
`PostgreSQLDatabaseTasks#structure_dump`, which Rails
|
|
547
|
-
offers no extension point for. A Rails upgrade that
|
|
548
|
-
test suite's round trip (`test/schema_dump_test.rb`).
|
|
591
|
+
`.load_schema`, and `PostgreSQLDatabaseTasks#structure_dump`, which Rails
|
|
592
|
+
marks `:nodoc:` and offers no extension point for. A Rails upgrade that
|
|
593
|
+
renames them fails the test suite's round trip (`test/schema_dump_test.rb`).
|
|
549
594
|
|
|
550
595
|
## Layout
|
|
551
596
|
|
data/ext/trellis_ruby/Cargo.lock
CHANGED
|
@@ -1729,8 +1729,8 @@ dependencies = [
|
|
|
1729
1729
|
|
|
1730
1730
|
[[package]]
|
|
1731
1731
|
name = "trellis"
|
|
1732
|
-
version = "0.
|
|
1733
|
-
source = "git+https://github.com/salesforce-misc/trellis?rev=
|
|
1732
|
+
version = "0.2.0"
|
|
1733
|
+
source = "git+https://github.com/salesforce-misc/trellis?rev=f2afc74f5ee0a6ac309051da8678e0b2d862892e#f2afc74f5ee0a6ac309051da8678e0b2d862892e"
|
|
1734
1734
|
dependencies = [
|
|
1735
1735
|
"deadpool",
|
|
1736
1736
|
"deadpool-postgres",
|
|
@@ -1739,7 +1739,6 @@ dependencies = [
|
|
|
1739
1739
|
"metrics-exporter-prometheus",
|
|
1740
1740
|
"metrics-util",
|
|
1741
1741
|
"refinery",
|
|
1742
|
-
"regex",
|
|
1743
1742
|
"rustls",
|
|
1744
1743
|
"serde",
|
|
1745
1744
|
"serde_json",
|
|
@@ -1752,8 +1751,8 @@ dependencies = [
|
|
|
1752
1751
|
|
|
1753
1752
|
[[package]]
|
|
1754
1753
|
name = "trellis-embed"
|
|
1755
|
-
version = "0.
|
|
1756
|
-
source = "git+https://github.com/salesforce-misc/trellis?rev=
|
|
1754
|
+
version = "0.2.0"
|
|
1755
|
+
source = "git+https://github.com/salesforce-misc/trellis?rev=f2afc74f5ee0a6ac309051da8678e0b2d862892e#f2afc74f5ee0a6ac309051da8678e0b2d862892e"
|
|
1757
1756
|
dependencies = [
|
|
1758
1757
|
"tracing",
|
|
1759
1758
|
"trellis",
|
|
@@ -1761,7 +1760,7 @@ dependencies = [
|
|
|
1761
1760
|
|
|
1762
1761
|
[[package]]
|
|
1763
1762
|
name = "trellis_ruby"
|
|
1764
|
-
version = "0.
|
|
1763
|
+
version = "0.2.0"
|
|
1765
1764
|
dependencies = [
|
|
1766
1765
|
"magnus",
|
|
1767
1766
|
"rb-sys",
|
data/ext/trellis_ruby/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "trellis_ruby"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
publish = false
|
|
6
6
|
|
|
@@ -28,8 +28,8 @@ doctest = false
|
|
|
28
28
|
ruby = ["dep:magnus", "dep:rb-sys"]
|
|
29
29
|
|
|
30
30
|
[dependencies]
|
|
31
|
-
trellis = { git = "https://github.com/salesforce-misc/trellis", rev = "
|
|
32
|
-
trellis-embed = { git = "https://github.com/salesforce-misc/trellis", rev = "
|
|
31
|
+
trellis = { git = "https://github.com/salesforce-misc/trellis", rev = "f2afc74f5ee0a6ac309051da8678e0b2d862892e" }
|
|
32
|
+
trellis-embed = { git = "https://github.com/salesforce-misc/trellis", rev = "f2afc74f5ee0a6ac309051da8678e0b2d862892e" }
|
|
33
33
|
magnus = { version = "0.9", optional = true, default-features = false, features = ["rb-sys"] }
|
|
34
34
|
rb-sys = { version = "0.9", optional = true }
|
|
35
35
|
|
data/ext/trellis_ruby/src/lib.rs
CHANGED
|
@@ -61,14 +61,17 @@ use magnus::{
|
|
|
61
61
|
};
|
|
62
62
|
use trellis::{BlockingTrellis, Config, ErrorCode, PendingCall, TrellisOptions};
|
|
63
63
|
use trellis_embed::{
|
|
64
|
-
DIVERGENCE_KINDS, ERROR_CODES,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
DIVERGENCE_KINDS, ERROR_CODES, JOIN_BUILD_STEPS, JOIN_SIDE_PARTS, JOIN_SIDES, PlainApplied,
|
|
65
|
+
PlainBackfillFailure, PlainBelowCatalogFloor, PlainCaptureFailure, PlainCaptureWait,
|
|
66
|
+
PlainConfig, PlainCorrectingNode, PlainDefinitionStatus, PlainDefinitionSummary,
|
|
67
|
+
PlainDivergence, PlainDrainFailure, PlainError, PlainExtremeIndexGap, PlainFanOutWarning,
|
|
68
|
+
PlainHeldKeys, PlainHiddenTable, PlainJoinBuildStep, PlainJoinStatus, PlainOtherProcess,
|
|
69
|
+
PlainPartialsWarning, PlainPoisonEntry, PlainQuarantineEntry, PlainSamplePage,
|
|
70
|
+
PlainSelfCheckJob, PlainSelfCheckReport, PlainSourceIndexGap, PlainVersionHandover,
|
|
71
|
+
SELF_CHECK_MODES, SELF_CHECK_OUTCOMES, SELF_CHECK_STATES, capture_failure_kind_names,
|
|
72
|
+
decode_cursor, decode_watermark, encode_watermark, index_role_names, quarantine_state_names,
|
|
73
|
+
require_transform_statement, self_check_mode, system_time_from_epoch_micros,
|
|
74
|
+
transform_status_names,
|
|
72
75
|
};
|
|
73
76
|
|
|
74
77
|
/// What a blocking call produces: its value, or the engine's error as plain
|
|
@@ -234,6 +237,29 @@ impl Handle {
|
|
|
234
237
|
rb_self.call(ruby, BlockingTrellis::start_dump_definitions_sql)
|
|
235
238
|
}
|
|
236
239
|
|
|
240
|
+
/// What a schema dump of the database needs from this instance
|
|
241
|
+
/// ([`BlockingTrellis::schema_dump`]): `[pg_dump_args, appendix]`, the
|
|
242
|
+
/// arguments that leave Trellis's own tables out of `pg_dump` and the
|
|
243
|
+
/// SQL to append after its filtered output.
|
|
244
|
+
fn schema_dump(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
|
|
245
|
+
let dump = rb_self.call(ruby, BlockingTrellis::start_schema_dump)?;
|
|
246
|
+
let args = ruby.ary_from_iter(dump.pg_dump_args);
|
|
247
|
+
Ok(ruby.ary_new_from_values(&[args.as_value(), ruby.str_new(&dump.appendix).as_value()]))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// `pg_dump_output` without this instance's capture triggers
|
|
251
|
+
/// ([`BlockingTrellis::filter_schema_dump`]). Reads nothing from the
|
|
252
|
+
/// database.
|
|
253
|
+
fn filter_schema_dump(
|
|
254
|
+
ruby: &Ruby,
|
|
255
|
+
rb_self: &Self,
|
|
256
|
+
pg_dump_output: String,
|
|
257
|
+
) -> Result<String, Error> {
|
|
258
|
+
rb_self
|
|
259
|
+
.with_engine(ruby, |trellis| trellis.filter_schema_dump(&pg_dump_output))?
|
|
260
|
+
.map_err(|err| raise(ruby, err.into()))
|
|
261
|
+
}
|
|
262
|
+
|
|
237
263
|
/// Rebuilds every applying definition that reads `source_table`
|
|
238
264
|
/// (`live -> backfilling`).
|
|
239
265
|
fn request_backfill(ruby: &Ruby, rb_self: &Self, source_table: String) -> Result<(), Error> {
|
|
@@ -835,6 +861,9 @@ fn symbol_words() -> impl Iterator<Item = &'static str> {
|
|
|
835
861
|
.chain(DIVERGENCE_KINDS)
|
|
836
862
|
.chain(capture_failure_kind_names())
|
|
837
863
|
.chain(index_role_names())
|
|
864
|
+
.chain(JOIN_BUILD_STEPS)
|
|
865
|
+
.chain(JOIN_SIDES)
|
|
866
|
+
.chain(JOIN_SIDE_PARTS)
|
|
838
867
|
}
|
|
839
868
|
|
|
840
869
|
/// A hash from symbol keys to `fields`' values: the shape every record
|
|
@@ -906,8 +935,21 @@ fn status_hash(ruby: &Ruby, status: PlainDefinitionStatus) -> Result<RHash, Erro
|
|
|
906
935
|
.map(|failure| drain_failure_hash(ruby, failure))
|
|
907
936
|
.transpose()?;
|
|
908
937
|
let source_index_gaps = array(ruby, status.source_index_gaps, source_index_gap_hash)?;
|
|
938
|
+
let extreme_index_gaps = array(ruby, status.extreme_index_gaps, extreme_index_gap_hash)?;
|
|
909
939
|
let correcting = array(ruby, status.correcting, correcting_hash)?;
|
|
910
940
|
let paused_upstream_columns = ruby.ary_from_iter(status.paused_upstream_columns);
|
|
941
|
+
let version_handover = status
|
|
942
|
+
.version_handover
|
|
943
|
+
.map(|seen| version_handover_hash(ruby, seen))
|
|
944
|
+
.transpose()?;
|
|
945
|
+
let below_catalog_floor = status
|
|
946
|
+
.below_catalog_floor
|
|
947
|
+
.map(|seen| below_catalog_floor_hash(ruby, seen))
|
|
948
|
+
.transpose()?;
|
|
949
|
+
let join = status
|
|
950
|
+
.join
|
|
951
|
+
.map(|join| join_status_hash(ruby, join))
|
|
952
|
+
.transpose()?;
|
|
911
953
|
record(
|
|
912
954
|
ruby,
|
|
913
955
|
[
|
|
@@ -918,11 +960,142 @@ fn status_hash(ruby: &Ruby, status: PlainDefinitionStatus) -> Result<RHash, Erro
|
|
|
918
960
|
("held_keys", ruby.into_value(held_keys)),
|
|
919
961
|
("drain_failure", ruby.into_value(drain_failure)),
|
|
920
962
|
("source_index_gaps", source_index_gaps.as_value()),
|
|
963
|
+
("extreme_index_gaps", extreme_index_gaps.as_value()),
|
|
921
964
|
("correcting", correcting.as_value()),
|
|
922
965
|
(
|
|
923
966
|
"paused_upstream_columns",
|
|
924
967
|
paused_upstream_columns.as_value(),
|
|
925
968
|
),
|
|
969
|
+
("build_wait_xid", ruby.into_value(status.build_wait_xid)),
|
|
970
|
+
("version_handover", ruby.into_value(version_handover)),
|
|
971
|
+
("below_catalog_floor", ruby.into_value(below_catalog_floor)),
|
|
972
|
+
("join", ruby.into_value(join)),
|
|
973
|
+
("floor", ruby.into_value(status.floor)),
|
|
974
|
+
],
|
|
975
|
+
)
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/// A `JOIN` transform's hidden tables, warnings and build step.
|
|
979
|
+
fn join_status_hash(ruby: &Ruby, join: PlainJoinStatus) -> Result<RHash, Error> {
|
|
980
|
+
let hidden_tables = array(ruby, join.hidden_tables, hidden_table_hash)?;
|
|
981
|
+
let partials_warning = join
|
|
982
|
+
.partials_warning
|
|
983
|
+
.map(|warning| partials_warning_hash(ruby, warning))
|
|
984
|
+
.transpose()?;
|
|
985
|
+
let fan_out_warnings = array(ruby, join.fan_out_warnings, fan_out_warning_hash)?;
|
|
986
|
+
let building = join
|
|
987
|
+
.building
|
|
988
|
+
.map(|step| join_build_step_hash(ruby, step))
|
|
989
|
+
.transpose()?;
|
|
990
|
+
record(
|
|
991
|
+
ruby,
|
|
992
|
+
[
|
|
993
|
+
("hidden_tables", hidden_tables.as_value()),
|
|
994
|
+
("partials_warning", ruby.into_value(partials_warning)),
|
|
995
|
+
("fan_out_warnings", fan_out_warnings.as_value()),
|
|
996
|
+
("building", ruby.into_value(building)),
|
|
997
|
+
],
|
|
998
|
+
)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/// One hidden table of a `JOIN`.
|
|
1002
|
+
fn hidden_table_hash(ruby: &Ruby, table: PlainHiddenTable) -> Result<RHash, Error> {
|
|
1003
|
+
record(
|
|
1004
|
+
ruby,
|
|
1005
|
+
[
|
|
1006
|
+
("table", ruby.into_value(table.table)),
|
|
1007
|
+
("name", ruby.into_value(table.name)),
|
|
1008
|
+
("estimated_rows", ruby.into_value(table.estimated_rows)),
|
|
1009
|
+
("bytes", ruby.into_value(table.bytes)),
|
|
1010
|
+
],
|
|
1011
|
+
)
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/// `P` has grown close to the fact table's size.
|
|
1015
|
+
fn partials_warning_hash(ruby: &Ruby, warning: PlainPartialsWarning) -> Result<RHash, Error> {
|
|
1016
|
+
record(
|
|
1017
|
+
ruby,
|
|
1018
|
+
[
|
|
1019
|
+
("partials_rows", ruby.into_value(warning.partials_rows)),
|
|
1020
|
+
("fact_rows", ruby.into_value(warning.fact_rows)),
|
|
1021
|
+
],
|
|
1022
|
+
)
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/// A 1-1 `JOIN`'s very large group on one join key at one joined position.
|
|
1026
|
+
fn fan_out_warning_hash(ruby: &Ruby, warning: PlainFanOutWarning) -> Result<RHash, Error> {
|
|
1027
|
+
record(
|
|
1028
|
+
ruby,
|
|
1029
|
+
[
|
|
1030
|
+
("position", ruby.into_value(warning.position)),
|
|
1031
|
+
("key", ruby.into_value(warning.key)),
|
|
1032
|
+
("rows", ruby.into_value(warning.rows)),
|
|
1033
|
+
("threshold", ruby.into_value(warning.threshold)),
|
|
1034
|
+
],
|
|
1035
|
+
)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/// The step a `JOIN` build runs. `step`, `side` and `part` are symbols from
|
|
1039
|
+
/// [`JOIN_BUILD_STEPS`], [`JOIN_SIDES`] and [`JOIN_SIDE_PARTS`]; each of the
|
|
1040
|
+
/// others is set only for the step that carries it.
|
|
1041
|
+
fn join_build_step_hash(ruby: &Ruby, step: PlainJoinBuildStep) -> Result<RHash, Error> {
|
|
1042
|
+
record(
|
|
1043
|
+
ruby,
|
|
1044
|
+
[
|
|
1045
|
+
("step", word_symbol(ruby, step.step).as_value()),
|
|
1046
|
+
(
|
|
1047
|
+
"side",
|
|
1048
|
+
ruby.into_value(step.side.map(|word| word_symbol(ruby, word))),
|
|
1049
|
+
),
|
|
1050
|
+
(
|
|
1051
|
+
"part",
|
|
1052
|
+
ruby.into_value(step.part.map(|word| word_symbol(ruby, word))),
|
|
1053
|
+
),
|
|
1054
|
+
("leaf", ruby.into_value(step.leaf)),
|
|
1055
|
+
("remaining", ruby.into_value(step.remaining)),
|
|
1056
|
+
("position", ruby.into_value(step.position)),
|
|
1057
|
+
],
|
|
1058
|
+
)
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/// Another Trellis version is live, so this binary stands by.
|
|
1062
|
+
fn version_handover_hash(ruby: &Ruby, seen: PlainVersionHandover) -> Result<RHash, Error> {
|
|
1063
|
+
let processes = array(ruby, seen.processes, other_process_hash)?;
|
|
1064
|
+
record(
|
|
1065
|
+
ruby,
|
|
1066
|
+
[
|
|
1067
|
+
("this_version", ruby.into_value(seen.this_version)),
|
|
1068
|
+
("processes", processes.as_value()),
|
|
1069
|
+
(
|
|
1070
|
+
"unregistered_staging_worker",
|
|
1071
|
+
ruby.into_value(seen.unregistered_staging_worker),
|
|
1072
|
+
),
|
|
1073
|
+
(
|
|
1074
|
+
"migrations_pending",
|
|
1075
|
+
ruby.into_value(seen.migrations_pending),
|
|
1076
|
+
),
|
|
1077
|
+
],
|
|
1078
|
+
)
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/// This binary is below the catalog floor, so it stands by.
|
|
1082
|
+
fn below_catalog_floor_hash(ruby: &Ruby, seen: PlainBelowCatalogFloor) -> Result<RHash, Error> {
|
|
1083
|
+
record(
|
|
1084
|
+
ruby,
|
|
1085
|
+
[
|
|
1086
|
+
("floor", ruby.into_value(seen.floor)),
|
|
1087
|
+
("this_version", ruby.into_value(seen.this_version)),
|
|
1088
|
+
],
|
|
1089
|
+
)
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/// A live process of another version.
|
|
1093
|
+
fn other_process_hash(ruby: &Ruby, process: PlainOtherProcess) -> Result<RHash, Error> {
|
|
1094
|
+
record(
|
|
1095
|
+
ruby,
|
|
1096
|
+
[
|
|
1097
|
+
("worker_id", ruby.into_value(process.worker_id)),
|
|
1098
|
+
("version", ruby.into_value(process.version)),
|
|
926
1099
|
],
|
|
927
1100
|
)
|
|
928
1101
|
}
|
|
@@ -953,6 +1126,20 @@ fn source_index_gap_hash(ruby: &Ruby, gap: PlainSourceIndexGap) -> Result<RHash,
|
|
|
953
1126
|
)
|
|
954
1127
|
}
|
|
955
1128
|
|
|
1129
|
+
/// A source index a `MIN`/`MAX` field's probe reads and the source lacks.
|
|
1130
|
+
fn extreme_index_gap_hash(ruby: &Ruby, gap: PlainExtremeIndexGap) -> Result<RHash, Error> {
|
|
1131
|
+
record(
|
|
1132
|
+
ruby,
|
|
1133
|
+
[
|
|
1134
|
+
("table", ruby.into_value(gap.table)),
|
|
1135
|
+
("columns", ruby.into_value(gap.columns)),
|
|
1136
|
+
("argument", ruby.into_value(gap.argument)),
|
|
1137
|
+
("opted_out", ruby.into_value(gap.opted_out)),
|
|
1138
|
+
("fix", ruby.into_value(gap.fix)),
|
|
1139
|
+
],
|
|
1140
|
+
)
|
|
1141
|
+
}
|
|
1142
|
+
|
|
956
1143
|
/// A drain page that keeps failing with nothing charged or paused.
|
|
957
1144
|
fn drain_failure_hash(ruby: &Ruby, failure: PlainDrainFailure) -> Result<RHash, Error> {
|
|
958
1145
|
record(
|
|
@@ -1222,6 +1409,21 @@ fn index_roles(ruby: &Ruby) -> RArray {
|
|
|
1222
1409
|
word_symbols(ruby, index_role_names())
|
|
1223
1410
|
}
|
|
1224
1411
|
|
|
1412
|
+
/// Every step symbol a `JOIN` build's status can carry.
|
|
1413
|
+
fn join_build_steps(ruby: &Ruby) -> RArray {
|
|
1414
|
+
word_symbols(ruby, JOIN_BUILD_STEPS)
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/// Every side symbol a `JOIN` build step can carry.
|
|
1418
|
+
fn join_sides(ruby: &Ruby) -> RArray {
|
|
1419
|
+
word_symbols(ruby, JOIN_SIDES)
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/// Every part symbol a `JOIN` build's side step can carry.
|
|
1423
|
+
fn join_side_parts(ruby: &Ruby) -> RArray {
|
|
1424
|
+
word_symbols(ruby, JOIN_SIDE_PARTS)
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1225
1427
|
/// Every statement kind `trellis`'s grammar has, for the test asserting
|
|
1226
1428
|
/// `apply`'s round trip covers each one.
|
|
1227
1429
|
fn statement_kinds() -> Vec<&'static str> {
|
|
@@ -1265,6 +1467,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1265
1467
|
native.define_module_function("divergence_kinds", function!(divergence_kinds, 0))?;
|
|
1266
1468
|
native.define_module_function("capture_failure_kinds", function!(capture_failure_kinds, 0))?;
|
|
1267
1469
|
native.define_module_function("index_roles", function!(index_roles, 0))?;
|
|
1470
|
+
native.define_module_function("join_build_steps", function!(join_build_steps, 0))?;
|
|
1471
|
+
native.define_module_function("join_sides", function!(join_sides, 0))?;
|
|
1472
|
+
native.define_module_function("join_side_parts", function!(join_side_parts, 0))?;
|
|
1268
1473
|
native.define_module_function("statement_kinds", function!(statement_kinds, 0))?;
|
|
1269
1474
|
native.define_module_function("render_prometheus", function!(render_prometheus, 0))?;
|
|
1270
1475
|
|
|
@@ -1279,6 +1484,8 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1279
1484
|
"dump_definitions_sql",
|
|
1280
1485
|
method!(Handle::dump_definitions_sql, 0),
|
|
1281
1486
|
)?;
|
|
1487
|
+
handle.define_method("schema_dump", method!(Handle::schema_dump, 0))?;
|
|
1488
|
+
handle.define_method("filter_schema_dump", method!(Handle::filter_schema_dump, 1))?;
|
|
1282
1489
|
handle.define_method("request_backfill", method!(Handle::request_backfill, 1))?;
|
|
1283
1490
|
handle.define_method("release_key", method!(Handle::release_key, 3))?;
|
|
1284
1491
|
handle.define_method("poisoned_since", method!(Handle::poisoned_since, 1))?;
|
data/lib/trellis/error.rb
CHANGED
|
@@ -14,7 +14,8 @@ module Trellis
|
|
|
14
14
|
CODE = nil
|
|
15
15
|
|
|
16
16
|
# One of :parse, :validation, :connectivity, :conflict, :not_found,
|
|
17
|
-
# :internal, :timeout, or :unknown for
|
|
17
|
+
# :internal, :timeout, :version_handover, :version_floor, or :unknown for
|
|
18
|
+
# a code newer than this binding.
|
|
18
19
|
def code
|
|
19
20
|
self.class::CODE
|
|
20
21
|
end
|
|
@@ -62,12 +63,33 @@ module Trellis
|
|
|
62
63
|
|
|
63
64
|
# A bounded wait ran out of time, such as Trellis.await_converged's
|
|
64
65
|
# timeout_ms, or Trellis.release_key's wait for the pages in flight on its
|
|
65
|
-
# table, or a
|
|
66
|
+
# table, or a statement waited out the 30-second lock timeout behind another
|
|
67
|
+
# transaction's lock (a define, ALTER or DROP behind a drain page, say), or a
|
|
68
|
+
# call ran out of its 30-second deadline (the server stopped
|
|
66
69
|
# what the call started and rolled it back). Expected, not a bug: retry it.
|
|
67
70
|
class TimeoutError < Error
|
|
68
71
|
CODE = :timeout
|
|
69
72
|
end
|
|
70
73
|
|
|
74
|
+
# A Trellis process of another version is live on the instance, so this
|
|
75
|
+
# call changed nothing: a state-changing call, or Trellis.migrate with
|
|
76
|
+
# migrations pending, during a deploy that crosses a version boundary.
|
|
77
|
+
# Read-only calls keep working. Retry once the other version's processes
|
|
78
|
+
# have stopped; the message names the versions and their worker ids.
|
|
79
|
+
class VersionHandoverError < Error
|
|
80
|
+
CODE = :version_handover
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# This Trellis version is below a compatibility floor the database
|
|
84
|
+
# records: the catalog floor, the oldest version that can read the
|
|
85
|
+
# instance's catalog, so Trellis.migrate or a state-changing call refused
|
|
86
|
+
# it; or a definition's floor, the oldest version that can run it, so DROP
|
|
87
|
+
# TRANSFORM refused it and changed nothing. Not retryable with this
|
|
88
|
+
# version: run the version the message names, or a later one.
|
|
89
|
+
class VersionFloorError < Error
|
|
90
|
+
CODE = :version_floor
|
|
91
|
+
end
|
|
92
|
+
|
|
71
93
|
# An error code newer than this binding. The message names the code.
|
|
72
94
|
class UnknownError < Error
|
|
73
95
|
CODE = :unknown
|
|
@@ -97,7 +119,9 @@ module Trellis
|
|
|
97
119
|
"conflict" => ConflictError,
|
|
98
120
|
"not_found" => NotFoundError,
|
|
99
121
|
"internal" => InternalError,
|
|
100
|
-
"timeout" => TimeoutError
|
|
122
|
+
"timeout" => TimeoutError,
|
|
123
|
+
"version_handover" => VersionHandoverError,
|
|
124
|
+
"version_floor" => VersionFloorError
|
|
101
125
|
}.freeze
|
|
102
126
|
end
|
|
103
127
|
end
|
data/lib/trellis/instance.rb
CHANGED
|
@@ -65,7 +65,8 @@ module Trellis
|
|
|
65
65
|
# TRANSFORM) and reports what it did
|
|
66
66
|
# as an Applied. define is the one guarded convenience: an apply that
|
|
67
67
|
# refuses anything but a TRANSFORM statement before applying it.
|
|
68
|
-
# - Reads: status, definitions,
|
|
68
|
+
# - Reads: status, definitions, schema_dump, filter_schema_dump,
|
|
69
|
+
# dump_definitions_sql.
|
|
69
70
|
# - Quarantine: quarantined, quarantine_status, sample_quarantined,
|
|
70
71
|
# poisoned_since, release_key. Pausing and resuming are statements:
|
|
71
72
|
# instance.apply("RESUME TRANSFORM order_totals.total").
|
|
@@ -336,6 +337,35 @@ module Trellis
|
|
|
336
337
|
handle.dump_definitions_sql
|
|
337
338
|
end
|
|
338
339
|
|
|
340
|
+
# What a schema dump of this instance's database needs, as a frozen Hash:
|
|
341
|
+
# :pg_dump_args, the arguments (each one argument, not shell words) that
|
|
342
|
+
# leave Trellis's own schema and the 1-1 targets' ledgers out of
|
|
343
|
+
# `pg_dump`, and :appendix, the SQL to append after the dump. Run
|
|
344
|
+
# `pg_dump --schema-only` with pg_dump_args (and the target schema among
|
|
345
|
+
# the dump's `--schema`s, if it names any), pass the output through
|
|
346
|
+
# filter_schema_dump, and append the appendix. Both are the same for the
|
|
347
|
+
# same definitions, whatever their history. A database loaded from the file has the application's
|
|
348
|
+
# schema and the target tables; `migrate` restores every definition into
|
|
349
|
+
# its target, each unbuilt, and the worker builds them. Trellis's own
|
|
350
|
+
# schema, the capture triggers and the ledgers are not in the dump.
|
|
351
|
+
#
|
|
352
|
+
# Read from one snapshot, a different one from the pg_dump's: the Rails
|
|
353
|
+
# hook (Trellis::SchemaDump) reads it on both sides of the pg_dump and
|
|
354
|
+
# retries when the two differ.
|
|
355
|
+
def schema_dump
|
|
356
|
+
pg_dump_args, appendix = handle.schema_dump
|
|
357
|
+
{ pg_dump_args: pg_dump_args.freeze, appendix: appendix.freeze }.freeze
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# pg_dump_output, the output of a `pg_dump` run with schema_dump's
|
|
361
|
+
# pg_dump_args, without this instance's capture triggers on its source
|
|
362
|
+
# tables, which `pg_dump` can't leave out by itself. Reads nothing from
|
|
363
|
+
# the database. Raises Trellis::InternalError if a line still runs one of
|
|
364
|
+
# Trellis's functions afterwards: the dump wouldn't load.
|
|
365
|
+
def filter_schema_dump(pg_dump_output)
|
|
366
|
+
handle.filter_schema_dump(pg_dump_output)
|
|
367
|
+
end
|
|
368
|
+
|
|
339
369
|
# Rebuilds every definition that reads source_table (a bare table name,
|
|
340
370
|
# resolved like one in a statement), and returns once that rebuild is
|
|
341
371
|
# queued.
|
data/lib/trellis/pg.rb
CHANGED
|
@@ -43,7 +43,8 @@ require_relative "metrics"
|
|
|
43
43
|
#
|
|
44
44
|
# - Lifecycle: connect, connected?, migrate, config, shutdown.
|
|
45
45
|
# - Every statement form: apply, and define for a TRANSFORM statement alone.
|
|
46
|
-
# - Reads: status, definitions,
|
|
46
|
+
# - Reads: status, definitions, schema_dump, filter_schema_dump,
|
|
47
|
+
# dump_definitions_sql.
|
|
47
48
|
# - Quarantine: quarantined, quarantine_status, sample_quarantined,
|
|
48
49
|
# poisoned_since, release_key.
|
|
49
50
|
# - Operations: request_backfill, detect_capture_disruptions, has_live_drain_workers?,
|
data/lib/trellis/schema_dump.rb
CHANGED
|
@@ -2,46 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
require "active_record"
|
|
4
4
|
require "etc"
|
|
5
|
+
require "fileutils"
|
|
5
6
|
require "trellis/pg"
|
|
6
7
|
|
|
7
8
|
module Trellis
|
|
8
9
|
# Makes `structure.sql` carry Trellis's definitions, the way Rails makes it
|
|
9
10
|
# carry `schema_migrations`: after `pg_dump` writes the schema, the dump
|
|
10
|
-
# appends the
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# appends the rows that describe how the schema came to be. The dump has
|
|
12
|
+
# the application's schema and the target tables, and one row per
|
|
13
|
+
# definition (Trellis::Instance#schema_dump). Trellis's own schema, the
|
|
14
|
+
# capture triggers on source tables and the 1-1 targets' ledgers are left
|
|
15
|
+
# out: a database built from the file gets them from the loading release,
|
|
16
|
+
# whose `migrate` restores each definition into its dumped target (ADR-0025).
|
|
14
17
|
#
|
|
15
18
|
# Needs `config.active_record.schema_format = :sql`: a `schema.rb` can't
|
|
16
|
-
# carry
|
|
19
|
+
# carry the definitions. With `:ruby`, the dump raises
|
|
17
20
|
# Trellis::ValidationError and writes nothing: a `schema.rb` would load
|
|
18
21
|
# into a database with target tables and no definitions.
|
|
19
22
|
#
|
|
20
|
-
# It hooks the
|
|
21
|
-
# `db:schema:dump` task and the dump after `db:migrate` call them):
|
|
23
|
+
# It hooks the places Rails offers no extension point for:
|
|
22
24
|
#
|
|
23
|
-
# - ActiveRecord::Tasks::DatabaseTasks.dump_schema
|
|
24
|
-
#
|
|
25
|
-
# the
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# `
|
|
25
|
+
# - ActiveRecord::Tasks::DatabaseTasks.dump_schema (the `db:schema:dump`
|
|
26
|
+
# task and the dump after `db:migrate` both call it), whose `:sql`
|
|
27
|
+
# branch is `pg_dump` followed by the `schema_migrations` rows. This
|
|
28
|
+
# hook takes the capture triggers out of the `pg_dump` output
|
|
29
|
+
# (Trellis::Instance#filter_schema_dump) and adds the definitions'
|
|
30
|
+
# rows after the `schema_migrations` ones, for the default instance and
|
|
31
|
+
# each of config.trellis.instances. Those rows are read before and after
|
|
32
|
+
# the `pg_dump`, which reads its own snapshot, and the dump is redone (up
|
|
33
|
+
# to three times) when a define or drop committed in between, so the DDL
|
|
34
|
+
# and the rows agree.
|
|
35
|
+
# - ActiveRecord::Tasks::PostgreSQLDatabaseTasks#structure_dump. This hook
|
|
36
|
+
# adds each instance's `pg_dump_args` (which exclude the Trellis schema
|
|
37
|
+
# and the ledgers) and, when Rails passes `--schema` for the search
|
|
38
|
+
# path's schemas, each instance's `target_schema`.
|
|
39
|
+
# - ActiveRecord::Tasks::DatabaseTasks.load_schema, which
|
|
40
|
+
# `db:schema:load`, `db:prepare` and the test database's
|
|
41
|
+
# `maintain_test_schema!` load the file with. After the load, this hook
|
|
42
|
+
# runs `migrate` for each configured instance, which restores the
|
|
43
|
+
# definitions (a file in the older format, which carries Trellis's
|
|
44
|
+
# schema, needs nothing more and loads as before). A load of another
|
|
45
|
+
# environment's database (`db:prepare` in development prepares the test
|
|
46
|
+
# database too, and `db:test:prepare` the test one) runs it against
|
|
47
|
+
# that database: the instances' options with the database's host, port,
|
|
48
|
+
# name, user and password from its config.
|
|
36
49
|
#
|
|
37
|
-
#
|
|
50
|
+
# A load that doesn't go through Rails (`psql -f structure.sql`) is followed
|
|
51
|
+
# by `rails trellis:migrate`.
|
|
52
|
+
#
|
|
53
|
+
# All three apply to one database: the config named by
|
|
38
54
|
# `config.trellis.dump_database` (default "primary"), the one
|
|
39
55
|
# config.trellis.connect and config.trellis.instances point at. The other
|
|
40
|
-
# databases of a multi-database app dump as Rails
|
|
56
|
+
# databases of a multi-database app dump and load as Rails does it.
|
|
41
57
|
#
|
|
42
58
|
# A dump lists definitions and nothing else of Trellis's state. Every
|
|
43
|
-
# definition loads
|
|
44
|
-
#
|
|
59
|
+
# definition loads unbuilt, as a `define` leaves it, including one that
|
|
60
|
+
# was paused or quarantined where the dump was taken.
|
|
45
61
|
#
|
|
46
62
|
# `pg_dump` 17 writes `CREATE SCHEMA public` when `public` is dumped,
|
|
47
63
|
# which fails to load into a database that has one (every database
|
|
@@ -54,12 +70,9 @@ module Trellis
|
|
|
54
70
|
# socket) matches, besides a socket directory.
|
|
55
71
|
LOCAL_HOSTS = %w[localhost 127.0.0.1 ::1].freeze
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
--
|
|
61
|
-
|
|
62
|
-
SQL
|
|
73
|
+
# The thread-local the dump hook leaves the parts it read before the
|
|
74
|
+
# `pg_dump` in, for the structure-dump hook's arguments.
|
|
75
|
+
PARTS_KEY = :trellis_schema_dump_parts
|
|
63
76
|
|
|
64
77
|
class << self
|
|
65
78
|
# Installs the hooks. Idempotent.
|
|
@@ -68,6 +81,12 @@ module Trellis
|
|
|
68
81
|
::ActiveRecord::Tasks::PostgreSQLDatabaseTasks.prepend(StructureDumpHook)
|
|
69
82
|
end
|
|
70
83
|
|
|
84
|
+
# The schema format of a dump or load of db_config: the format
|
|
85
|
+
# argument args carries, or Rails' default for db_config.
|
|
86
|
+
def format_of(db_config, args)
|
|
87
|
+
args.first || (db_config.respond_to?(:schema_format) ? db_config.schema_format : ::ActiveRecord.schema_format)
|
|
88
|
+
end
|
|
89
|
+
|
|
71
90
|
# Whether Trellis's definitions belong in the dump of db_config: Trellis
|
|
72
91
|
# is configured, and db_config is the database config.trellis.dump_database
|
|
73
92
|
# names.
|
|
@@ -122,38 +141,136 @@ module Trellis
|
|
|
122
141
|
(app&.config&.trellis&.dump_database || "primary").to_s
|
|
123
142
|
end
|
|
124
143
|
|
|
125
|
-
# The
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
# The flags to add to the pg_dump of db_config, whose connection
|
|
145
|
+
# settings are configuration_hash: each instance's pg_dump_args, and its
|
|
146
|
+
# target schema when Rails names the schemas to dump. parts are the
|
|
147
|
+
# instances' schema_dump Hashes (Trellis::Instance#schema_dump), read now when the dump hook
|
|
148
|
+
# hasn't.
|
|
149
|
+
def schema_flags(db_config, configuration_hash, parts = nil)
|
|
150
|
+
return [] unless applies?(db_config)
|
|
151
|
+
|
|
152
|
+
parts ||= Thread.current[PARTS_KEY] || self.parts
|
|
153
|
+
flags = []
|
|
154
|
+
if restricts_schemas?(configuration_hash)
|
|
155
|
+
flags.concat(configured_options.map { |options| "--schema=#{exact_pattern(options.fetch(:target_schema, "public"))}" })
|
|
156
|
+
end
|
|
157
|
+
flags.concat(parts.flat_map { |part| part.fetch(:pg_dump_args) })
|
|
158
|
+
flags.uniq
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# schema as a pg_dump pattern that matches exactly it: as is when it is
|
|
162
|
+
# lower case letters, digits and underscores, which pg_dump reads
|
|
163
|
+
# literally, and double-quoted otherwise, so its case and pattern
|
|
164
|
+
# characters (* ? .) are taken literally.
|
|
165
|
+
def exact_pattern(schema)
|
|
166
|
+
schema = schema.to_s
|
|
167
|
+
schema.match?(/\A[a-z_][a-z0-9_]*\z/) ? schema : "\"#{schema.gsub('"', '""')}\""
|
|
136
168
|
end
|
|
137
169
|
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
parts << Railtie.with_handle(&:dump_definitions_sql) if Railtie.connect_options
|
|
143
|
-
Railtie.instance_options.each_key { |name| parts << Railtie.with_handle(name, &:dump_definitions_sql) }
|
|
144
|
-
parts.join("\n")
|
|
170
|
+
# Each configured instance's schema_dump Hashes (Trellis::Instance#schema_dump): the default
|
|
171
|
+
# instance's first, then config.trellis.instances' in order.
|
|
172
|
+
def parts
|
|
173
|
+
each_instance.map { |name| Railtie.with_handle(name, &:schema_dump) }
|
|
145
174
|
end
|
|
146
175
|
|
|
147
|
-
#
|
|
148
|
-
# `CREATE SCHEMA public` loadable into a database that has one
|
|
149
|
-
|
|
176
|
+
# The dump at filename without each instance's capture triggers, and
|
|
177
|
+
# its `CREATE SCHEMA public` loadable into a database that has one,
|
|
178
|
+
# followed by each of parts' appendix.
|
|
179
|
+
def finish(filename, parts)
|
|
150
180
|
sql = File.read(filename, encoding: "utf-8")
|
|
151
181
|
sql = sql.gsub(/^CREATE SCHEMA public;$/, "CREATE SCHEMA IF NOT EXISTS public;")
|
|
152
|
-
|
|
182
|
+
each_instance.each do |name|
|
|
183
|
+
sql = Railtie.with_handle(name) { |instance| instance.filter_schema_dump(sql) }
|
|
184
|
+
end
|
|
185
|
+
appendix = parts.map { |part| part.fetch(:appendix) }.join("\n")
|
|
186
|
+
File.write(filename, "#{sql}\n#{appendix}", encoding: "utf-8")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Removes what a dump that failed left at filename, unless it is
|
|
190
|
+
# previous (the file's bytes before the dump, nil when there was none):
|
|
191
|
+
# a failure before `pg_dump` wrote to the file leaves it alone.
|
|
192
|
+
def discard_unfinished(filename, previous)
|
|
193
|
+
return unless File.exist?(filename)
|
|
194
|
+
return if previous && File.binread(filename) == previous
|
|
195
|
+
|
|
196
|
+
FileUtils.rm_f(filename)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Restores the definitions a load of db_config's dump queued: `migrate`
|
|
200
|
+
# on each configured instance, in the database db_config names.
|
|
201
|
+
def restore(db_config)
|
|
202
|
+
verify_same_database!(db_config)
|
|
203
|
+
if db_config.env_name.to_s == ::ActiveRecord::Tasks::DatabaseTasks.env.to_s
|
|
204
|
+
Railtie.migrate_all
|
|
205
|
+
return
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
configured = []
|
|
209
|
+
configured << [nil, Railtie.connect_options] if Railtie.connect_options
|
|
210
|
+
Railtie.instance_options.each { |name, options| configured << [name, options] }
|
|
211
|
+
configured.each do |name, options|
|
|
212
|
+
what = name ? "config.trellis.instances[#{name.inspect}]" : "config.trellis.connect"
|
|
213
|
+
url = environment_url(db_config, options[:url], what)
|
|
214
|
+
instance = Trellis::Instance.connect(**options, url:, staging: false, drain_threads: 0)
|
|
215
|
+
begin
|
|
216
|
+
instance.migrate
|
|
217
|
+
ensure
|
|
218
|
+
instance.shutdown
|
|
219
|
+
end
|
|
220
|
+
end
|
|
153
221
|
end
|
|
154
222
|
|
|
155
223
|
private
|
|
156
224
|
|
|
225
|
+
# nil for the default instance (when config.trellis.connect is set),
|
|
226
|
+
# then the name of each of config.trellis.instances.
|
|
227
|
+
def each_instance
|
|
228
|
+
names = []
|
|
229
|
+
names << nil if Railtie.connect_options
|
|
230
|
+
names.concat(Railtie.instance_options.keys)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def configured_options
|
|
234
|
+
options = []
|
|
235
|
+
options << Railtie.connect_options if Railtie.connect_options
|
|
236
|
+
options.concat(Railtie.instance_options.values)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# A libpq connection string for the database db_config names, in
|
|
240
|
+
# another environment than configured_url's: configured_url's
|
|
241
|
+
# settings with db_config's database, and its host, port, user,
|
|
242
|
+
# password and other libpq keywords (sslmode, sslrootcert, ...) where
|
|
243
|
+
# db_config names them (a host left to libpq's default keeps
|
|
244
|
+
# configured_url's, a local one by verify_same_database!). A db_config
|
|
245
|
+
# with a url is that url.
|
|
246
|
+
def environment_url(db_config, configured_url, what)
|
|
247
|
+
hash = db_config.configuration_hash.transform_keys(&:to_sym)
|
|
248
|
+
return hash[:url].to_s if hash[:url]
|
|
249
|
+
|
|
250
|
+
keywords = conninfo(configured_url, what).reject { |_, value| value.to_s.empty? }
|
|
251
|
+
host, port, dbname = rails_database(hash)
|
|
252
|
+
keywords["dbname"] = dbname
|
|
253
|
+
keywords["port"] = port
|
|
254
|
+
if host
|
|
255
|
+
keywords["host"] = host
|
|
256
|
+
# A hostaddr would win over the host for the connection itself.
|
|
257
|
+
keywords.delete("hostaddr")
|
|
258
|
+
end
|
|
259
|
+
user = hash[:username] || hash[:user]
|
|
260
|
+
keywords["user"] = user.to_s if user
|
|
261
|
+
# Every other libpq keyword the config names (sslmode, sslrootcert,
|
|
262
|
+
# connect_timeout, ...) is the connection Rails makes: the adapter
|
|
263
|
+
# hands them all to libpq. A blank one names nothing, a blank password
|
|
264
|
+
# included: it doesn't blank the url's.
|
|
265
|
+
PG::Connection.conndefaults_hash.each_key do |key|
|
|
266
|
+
next if %i[host port dbname user].include?(key)
|
|
267
|
+
|
|
268
|
+
value = hash[key]
|
|
269
|
+
keywords[key.to_s] = value.to_s unless value.nil? || value.to_s.empty?
|
|
270
|
+
end
|
|
271
|
+
keywords.map { |key, value| "#{key}='#{value.to_s.gsub(/[\\']/) { |c| "\\#{c}" }}'" }.join(" ")
|
|
272
|
+
end
|
|
273
|
+
|
|
157
274
|
# The keywords of a libpq connection string or URI. A failure names
|
|
158
275
|
# what was being read and never the string or libpq's message, which
|
|
159
276
|
# can quote part of a password (as Trellis's own parse does).
|
|
@@ -183,7 +300,8 @@ module Trellis
|
|
|
183
300
|
hash = hash.transform_keys(&:to_sym)
|
|
184
301
|
url = hash[:url] ? conninfo(hash[:url], "config.trellis.dump_database's url") : {}
|
|
185
302
|
given = { host: hash[:host] || url["host"], port: hash[:port] || url["port"],
|
|
186
|
-
dbname: hash[:database] ||
|
|
303
|
+
dbname: hash[:database] || hash[:dbname] || url["dbname"],
|
|
304
|
+
user: hash[:username] || hash[:user] || url["user"] }
|
|
187
305
|
defaults = PG::Connection.conndefaults_hash
|
|
188
306
|
resolved = given.to_h do |key, value|
|
|
189
307
|
value = defaults[key] if value.nil? || value.to_s.empty?
|
|
@@ -237,13 +355,11 @@ module Trellis
|
|
|
237
355
|
def dump_schema(db_config, *args)
|
|
238
356
|
return super unless SchemaDump.applies?(db_config) && db_config.schema_dump
|
|
239
357
|
|
|
240
|
-
format =
|
|
241
|
-
(db_config.respond_to?(:schema_format) ? db_config.schema_format : ::ActiveRecord.schema_format)
|
|
242
|
-
|
|
358
|
+
format = SchemaDump.format_of(db_config, args)
|
|
243
359
|
unless format.to_sym == :sql
|
|
244
360
|
raise Trellis::ValidationError,
|
|
245
361
|
"#{db_config.name}'s schema dump is #{format}, which can't carry Trellis's " \
|
|
246
|
-
"
|
|
362
|
+
"definitions: set config.active_record.schema_format = :sql"
|
|
247
363
|
end
|
|
248
364
|
|
|
249
365
|
filename = schema_dump_path(db_config, format)
|
|
@@ -251,17 +367,45 @@ module Trellis
|
|
|
251
367
|
|
|
252
368
|
SchemaDump.verify_same_database!(db_config)
|
|
253
369
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
SchemaDump.
|
|
259
|
-
|
|
370
|
+
previous = File.exist?(filename) ? File.binread(filename) : nil
|
|
371
|
+
finished = false
|
|
372
|
+
begin
|
|
373
|
+
SchemaDump::ATTEMPTS.times do
|
|
374
|
+
before = SchemaDump.parts
|
|
375
|
+
begin
|
|
376
|
+
Thread.current[SchemaDump::PARTS_KEY] = before
|
|
377
|
+
super(db_config, format)
|
|
378
|
+
ensure
|
|
379
|
+
Thread.current[SchemaDump::PARTS_KEY] = nil
|
|
380
|
+
end
|
|
381
|
+
if SchemaDump.parts == before
|
|
382
|
+
SchemaDump.finish(filename, before)
|
|
383
|
+
finished = true
|
|
384
|
+
return nil
|
|
385
|
+
end
|
|
260
386
|
end
|
|
387
|
+
raise Trellis::ConflictError,
|
|
388
|
+
"Trellis's definitions changed during each of #{SchemaDump::ATTEMPTS} dumps of " \
|
|
389
|
+
"#{db_config.name}: run the dump when no define or drop is running"
|
|
390
|
+
ensure
|
|
391
|
+
# Whatever Rails wrote is raw `pg_dump` output with the capture
|
|
392
|
+
# triggers in and no definitions: left in place, a later
|
|
393
|
+
# `db:schema:load` would load it without a word. A file no
|
|
394
|
+
# `pg_dump` wrote to (a failure before the first, or a `pg_dump`
|
|
395
|
+
# that failed before opening it) is the previous dump: left alone.
|
|
396
|
+
SchemaDump.discard_unfinished(filename, previous) unless finished
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# After Rails loads the dump database's `structure.sql`, restores the
|
|
401
|
+
# definitions it queued (SchemaDump.restore).
|
|
402
|
+
def load_schema(db_config, *args)
|
|
403
|
+
result = super
|
|
404
|
+
if SchemaDump.applies?(db_config) && db_config.schema_dump &&
|
|
405
|
+
SchemaDump.format_of(db_config, args).to_sym == :sql
|
|
406
|
+
SchemaDump.restore(db_config)
|
|
261
407
|
end
|
|
262
|
-
|
|
263
|
-
"Trellis's definitions changed during each of #{SchemaDump::ATTEMPTS} dumps of " \
|
|
264
|
-
"#{db_config.name}: run the dump when no define or drop is running"
|
|
408
|
+
result
|
|
265
409
|
end
|
|
266
410
|
end
|
|
267
411
|
|
data/lib/trellis/values.rb
CHANGED
|
@@ -78,6 +78,13 @@ module Trellis
|
|
|
78
78
|
# Trellis.self_check refuses to walk the target unless it opted out, when
|
|
79
79
|
# the walk scans the source once per page.
|
|
80
80
|
#
|
|
81
|
+
# extreme_index_gaps is a warning too: the ExtremeIndexGaps for the source
|
|
82
|
+
# indexes a MIN/MAX aggregate's probes read and the source lacks ([] when
|
|
83
|
+
# it has no MIN or MAX, or they exist). Define and a resume refuse a
|
|
84
|
+
# MIN/MAX definition without them, so a gap is an index dropped since.
|
|
85
|
+
# Nothing is paused: when a group's extreme is deleted, the next one is
|
|
86
|
+
# re-read by scanning the group instead, exact but slower.
|
|
87
|
+
#
|
|
81
88
|
# correcting lists the CorrectingNodes of its partitioned source it is being
|
|
82
89
|
# corrected for, oldest first ([] when none): a partition was attached,
|
|
83
90
|
# detached, dropped or truncated, and the definition re-derives the keys
|
|
@@ -92,15 +99,56 @@ module Trellis
|
|
|
92
99
|
# over its frozen values, so it is stale against the source. Nothing is
|
|
93
100
|
# paused or rebuilt: resuming the column writes through to the reader and
|
|
94
101
|
# it reads :live again.
|
|
102
|
+
#
|
|
103
|
+
# build_wait_xid is set while its build waits on something other than its
|
|
104
|
+
# own work: the transaction id of a fence. The build's source is another
|
|
105
|
+
# definition's target, and no chunk runs until every transaction that was
|
|
106
|
+
# open when the fence was taken has ended (a writer of that target could
|
|
107
|
+
# otherwise commit after a chunk's snapshot without the build seeing its
|
|
108
|
+
# rows). Look in pg_stat_activity for a transaction with a backend_xid at
|
|
109
|
+
# or below it; the wait clears on its own when that transaction ends. nil
|
|
110
|
+
# when the build waits on nothing.
|
|
111
|
+
#
|
|
112
|
+
# version_handover is set while a process of another Trellis version is
|
|
113
|
+
# live on the instance (a VersionHandover): this process stands by (it
|
|
114
|
+
# stages, drains and builds nothing) and its state-changing calls raise
|
|
115
|
+
# VersionHandoverError until the other version is gone. A process killed
|
|
116
|
+
# without a clean shutdown keeps its row live, and so delays the takeover,
|
|
117
|
+
# for at most 30 seconds. nil when none is.
|
|
118
|
+
#
|
|
119
|
+
# below_catalog_floor is set while this binary's version is below the
|
|
120
|
+
# instance's catalog floor, the oldest Trellis version that can read its
|
|
121
|
+
# catalog (a BelowCatalogFloor): this process stands by (it stages, drains
|
|
122
|
+
# and builds nothing, and checks again every standby poll) and its
|
|
123
|
+
# state-changing calls raise VersionFloorError. Run a version at or above
|
|
124
|
+
# the floor. nil when this binary meets it.
|
|
125
|
+
#
|
|
126
|
+
# join is set for a JOIN definition, aggregate or 1-1, and nil for any
|
|
127
|
+
# other (a JoinStatus). It never holds anything up: it changes nothing
|
|
128
|
+
# about the definition's status. It reports the estimated size of the
|
|
129
|
+
# definition's hidden tables, the P-size warning, the fan-out warning of a
|
|
130
|
+
# 1-1 JOIN and the step its build runs.
|
|
131
|
+
#
|
|
132
|
+
# floor is the oldest Trellis release that can run the definition, as
|
|
133
|
+
# "major.minor.patch": the latest release any construct, key space, option
|
|
134
|
+
# or hidden-table layout it uses arrived in. Trellis records it when the
|
|
135
|
+
# definition is defined, altered or rebuilt by a resume. Before rolling the
|
|
136
|
+
# application back to an earlier Trellis release, drop each definition
|
|
137
|
+
# whose floor is above that release, under the newer one.
|
|
95
138
|
Status = Data.define(:status, :backfill_failure, :capture_wait, :capture_failure,
|
|
96
139
|
:held_keys, :drain_failure, :source_index_gaps,
|
|
97
|
-
:correcting, :paused_upstream_columns
|
|
140
|
+
:extreme_index_gaps, :correcting, :paused_upstream_columns,
|
|
141
|
+
:build_wait_xid, :version_handover, :below_catalog_floor,
|
|
142
|
+
:join, :floor) do
|
|
98
143
|
def self.from_native(hash)
|
|
99
144
|
failure = hash[:backfill_failure]
|
|
100
145
|
wait = hash[:capture_wait]
|
|
101
146
|
capture_failure = hash[:capture_failure]
|
|
102
147
|
held_keys = hash[:held_keys]
|
|
103
148
|
drain_failure = hash[:drain_failure]
|
|
149
|
+
version_handover = hash[:version_handover]
|
|
150
|
+
below_catalog_floor = hash[:below_catalog_floor]
|
|
151
|
+
join = hash[:join]
|
|
104
152
|
new(status: hash[:status],
|
|
105
153
|
backfill_failure: failure && BackfillFailure.from_native(failure),
|
|
106
154
|
capture_wait: wait && CaptureWait.from_native(wait),
|
|
@@ -108,11 +156,116 @@ module Trellis
|
|
|
108
156
|
held_keys: held_keys && HeldKeys.from_native(held_keys),
|
|
109
157
|
drain_failure: drain_failure && DrainFailure.from_native(drain_failure),
|
|
110
158
|
source_index_gaps: hash.fetch(:source_index_gaps).map { |g| SourceIndexGap.new(**g) },
|
|
159
|
+
extreme_index_gaps: hash.fetch(:extreme_index_gaps).map { |g| ExtremeIndexGap.new(**g) },
|
|
111
160
|
correcting: hash.fetch(:correcting).map { |c| CorrectingNode.from_native(c) },
|
|
112
|
-
paused_upstream_columns: hash.fetch(:paused_upstream_columns)
|
|
161
|
+
paused_upstream_columns: hash.fetch(:paused_upstream_columns),
|
|
162
|
+
build_wait_xid: hash.fetch(:build_wait_xid),
|
|
163
|
+
version_handover: version_handover && VersionHandover.from_native(version_handover),
|
|
164
|
+
below_catalog_floor: below_catalog_floor && BelowCatalogFloor.new(**below_catalog_floor),
|
|
165
|
+
join: join && JoinStatus.from_native(join),
|
|
166
|
+
floor: hash.fetch(:floor))
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# A JOIN definition's hidden tables, as Trellis.status reports them: the
|
|
171
|
+
# estimated size of each (hidden_tables, HiddenTables: P, M, J and T, or R, M
|
|
172
|
+
# and J for a 1-1 JOIN, a table that doesn't exist left out), a warning when
|
|
173
|
+
# P has grown close to the fact table's size (partials_warning, a
|
|
174
|
+
# PartialsWarning), a 1-1 JOIN's warnings that one join key holds a very
|
|
175
|
+
# large group of rows (fan_out_warnings, FanOutWarnings, one per joined
|
|
176
|
+
# position past the threshold, in position order) and the step its build
|
|
177
|
+
# runs now, or runs next while no worker holds its job (building, a
|
|
178
|
+
# JoinBuildStep). partials_warning and building are nil, and
|
|
179
|
+
# fan_out_warnings is empty, when there is none.
|
|
180
|
+
# Sizes are the planner's estimates, as fresh as the table's last ANALYZE or
|
|
181
|
+
# autovacuum.
|
|
182
|
+
JoinStatus = Data.define(:hidden_tables, :partials_warning, :fan_out_warnings, :building) do
|
|
183
|
+
def self.from_native(hash)
|
|
184
|
+
partials_warning = hash.fetch(:partials_warning)
|
|
185
|
+
building = hash.fetch(:building)
|
|
186
|
+
new(hidden_tables: hash.fetch(:hidden_tables).map { |t| HiddenTable.new(**t) },
|
|
187
|
+
partials_warning: partials_warning && PartialsWarning.new(**partials_warning),
|
|
188
|
+
fan_out_warnings: hash.fetch(:fan_out_warnings).map { |w| FanOutWarning.new(**w) },
|
|
189
|
+
building: building && JoinBuildStep.new(**building))
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# One hidden table of a JOIN, in a JoinStatus: which table it is (table,
|
|
194
|
+
# "P", "M", "J", "T" or a 1-1 JOIN's "R"), its name in the catalog schema
|
|
195
|
+
# (name), its estimated live rows (estimated_rows, 0 before the first
|
|
196
|
+
# analyze) and its total size on disk with its indexes, in bytes (bytes).
|
|
197
|
+
HiddenTable = Data.define(:table, :name, :estimated_rows, :bytes)
|
|
198
|
+
|
|
199
|
+
# P, the FROM table grouped by the definition's GROUP BY columns and join
|
|
200
|
+
# key, has grown close to the fact table's size, so the JOIN costs about as
|
|
201
|
+
# much as the table it summarises: P's estimated rows (partials_rows) reach
|
|
202
|
+
# half of the fact table's (fact_rows).
|
|
203
|
+
PartialsWarning = Data.define(:partials_rows, :fact_rows)
|
|
204
|
+
|
|
205
|
+
# A 1-1 JOIN has a join key with a very large group of a rows at one joined
|
|
206
|
+
# position: a change of that position's row on the key rewrites that many
|
|
207
|
+
# target rows, in chunks, before a watermark token past it converges. It
|
|
208
|
+
# names the joined position (position, 1 for the first table joined), the
|
|
209
|
+
# join key's text (key, its columns'
|
|
210
|
+
# values joined by ","), the group's live rows (rows, exact up to ten times
|
|
211
|
+
# the threshold) and the threshold it passed (threshold). The group is found
|
|
212
|
+
# by sampling, so a group just past the threshold can go unreported until it
|
|
213
|
+
# grows.
|
|
214
|
+
FanOutWarning = Data.define(:position, :key, :rows, :threshold)
|
|
215
|
+
|
|
216
|
+
# The step a JOIN build runs, in a JoinStatus. step is the kind:
|
|
217
|
+
#
|
|
218
|
+
# * :whole, the whole build in one transaction, because neither side is a
|
|
219
|
+
# partitioned table with rows.
|
|
220
|
+
# * :side, a step of a build with a partitioned side. side is :from or
|
|
221
|
+
# :joined, and part says what it builds of that side: :whole, one :leaf
|
|
222
|
+
# (named by leaf, a schema.table; leaves build in oid order), the
|
|
223
|
+
# :remainder (the cells that route to none of its leaves) or :unroutable
|
|
224
|
+
# (the side's partition tree isn't recorded, or a leaf has no usable
|
|
225
|
+
# constraint text, so the build fails on this side and backfill_failure
|
|
226
|
+
# says why).
|
|
227
|
+
# * :final, the step that writes T and the target and completes the build.
|
|
228
|
+
# * A 1-1 JOIN's steps: :joined_rows, :joined_null_keys and :joined_gone for
|
|
229
|
+
# the pass over the M of the joined position position (1 for the first
|
|
230
|
+
# table joined), :planning_chunks while the plan job walks a's primary
|
|
231
|
+
# key, :chunks while the chunks of that key run (remaining of them are
|
|
232
|
+
# still to run), and :sweep_stale and :sweep_orphans for the two passes of
|
|
233
|
+
# the sweep that ends a rebuild in place.
|
|
234
|
+
# * :unknown, a step added since this binding was built; side and part read
|
|
235
|
+
# :unknown likewise for an unknown side or part.
|
|
236
|
+
#
|
|
237
|
+
# side, part, leaf, remaining and position are nil for the steps that don't
|
|
238
|
+
# carry them.
|
|
239
|
+
JoinBuildStep = Data.define(:step, :side, :part, :leaf, :remaining, :position)
|
|
240
|
+
|
|
241
|
+
# Another Trellis version is live on the instance, so this process changes
|
|
242
|
+
# nothing until it is gone, as Trellis.status reports it: this binary's
|
|
243
|
+
# version (this_version), the live processes holding it back (processes,
|
|
244
|
+
# OtherProcesses), whether the staging-worker lock is held by a session no
|
|
245
|
+
# registered process names, which is a staging worker of a release before
|
|
246
|
+
# 0.2.0 (unregistered_staging_worker), and whether the migrations this
|
|
247
|
+
# binary carries are pending (migrations_pending), in which case every
|
|
248
|
+
# live process holds it back, whatever its version.
|
|
249
|
+
VersionHandover = Data.define(:this_version, :processes, :unregistered_staging_worker,
|
|
250
|
+
:migrations_pending) do
|
|
251
|
+
def self.from_native(hash)
|
|
252
|
+
new(this_version: hash.fetch(:this_version),
|
|
253
|
+
processes: hash.fetch(:processes).map { |p| OtherProcess.new(**p) },
|
|
254
|
+
unregistered_staging_worker: hash.fetch(:unregistered_staging_worker),
|
|
255
|
+
migrations_pending: hash.fetch(:migrations_pending))
|
|
113
256
|
end
|
|
114
257
|
end
|
|
115
258
|
|
|
259
|
+
# A live process of another Trellis version, in a VersionHandover: its
|
|
260
|
+
# worker id (worker_id, trellis-<host>-<pid>-<random>) and its binary's
|
|
261
|
+
# version (version, nil for a release before 0.2.0, which records none).
|
|
262
|
+
OtherProcess = Data.define(:worker_id, :version)
|
|
263
|
+
|
|
264
|
+
# This binary's version is below the instance's catalog floor, so this
|
|
265
|
+
# process changes nothing, as Trellis.status reports it: the floor the
|
|
266
|
+
# catalog records (floor) and this binary's version (this_version).
|
|
267
|
+
BelowCatalogFloor = Data.define(:floor, :this_version)
|
|
268
|
+
|
|
116
269
|
# A node of a partitioned source a definition is being corrected for, as
|
|
117
270
|
# Trellis.status reports it: the schema-qualified node (node), when the
|
|
118
271
|
# staging worker queued its correction (since, a Time), and the keys still
|
|
@@ -135,6 +288,14 @@ module Trellis
|
|
|
135
288
|
# (fix). Trellis doesn't create indexes on your tables.
|
|
136
289
|
SourceIndexGap = Data.define(:table, :columns, :role, :opted_out, :fix)
|
|
137
290
|
|
|
291
|
+
# A source index a MIN/MAX field's probe reads and the source lacks, as
|
|
292
|
+
# Trellis.status reports it: the qualified table (table), the columns the
|
|
293
|
+
# index should lead with, the GROUP BY columns then the argument (columns),
|
|
294
|
+
# the source column the MIN/MAX reads (argument), whether the definition
|
|
295
|
+
# opted out with WITHOUT SOURCE INDEX (opted_out, which define refuses for
|
|
296
|
+
# MIN/MAX), and a create index statement to run (fix).
|
|
297
|
+
ExtremeIndexGap = Data.define(:table, :columns, :argument, :opted_out, :fix)
|
|
298
|
+
|
|
138
299
|
# A drain page that keeps failing with nothing charged or paused, as
|
|
139
300
|
# Trellis.status and Trellis.self_check report it: the segment whose page
|
|
140
301
|
# fails (seg_seq), the qualified source tables it holds changes to
|
|
@@ -175,7 +336,8 @@ module Trellis
|
|
|
175
336
|
end
|
|
176
337
|
|
|
177
338
|
# Why capture of source_table is broken, or why the drain (or a build's
|
|
178
|
-
# merge) halted on the definition: kind is :capture
|
|
339
|
+
# merge) halted on the definition: kind is :capture, :halt, :disruption, or
|
|
340
|
+
# :unknown for a kind a newer Trellis release stored, error is a
|
|
179
341
|
# sentence naming the cause, columns the columns it is about (empty when it
|
|
180
342
|
# isn't about a column, and for a halt), and detected_at (a Time) when it
|
|
181
343
|
# was first found.
|
data/lib/trellis/version.rb
CHANGED