trellis-pg 0.1.0.pre.alpha → 0.1.0.pre.beta

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1baafc626d091c77635ab5de6f1bb321200d27b3ce659465f2b69cf34e948ab
4
- data.tar.gz: c9c7d5f91121f749391c06e331732a375e98f149ef7ca2259aa2e965640ee6e2
3
+ metadata.gz: 040d3074f1223a81e28989c5d0a2b7825e84806ea0b088894fc1ea0daaf68312
4
+ data.tar.gz: e5bbfe7ce9446bd67d7b01666e555255a6ab39687c2c7415080789ddb8a934ac
5
5
  SHA512:
6
- metadata.gz: 685a0b5d491c23b32a9ed8784153d68ae60191f2901633030e74cd5da8f6fe19dfb3f09c1fdbfbf1d425e3a7023845f63726d7c37079f78d97086694de11e531
7
- data.tar.gz: c520497842ef2b4ecd252b3a307ea33c5a01b0fdd818f3f74561141c8b28408abb7288b50971383099979d6e074451febbcdfde047e6f3473f3e5b20eb31fb48
6
+ metadata.gz: 0f56d84b4f68c1619f480040e659513872e4c6d9f93358eff375b400b7e5b22eb9c2c29080c5433cc24ae9ab33cb8467f735c151e36c6634fea094fa2e04a5de
7
+ data.tar.gz: cc1c7b5b6ed393b91c6ed3a866a0172ddb085b297e4c7914ec4a3de56f32163734449d68a2d28e40192bc147c3b797cc16222ff4e7d2b943fbb97a84ac498847
data/README.md CHANGED
@@ -144,10 +144,33 @@ Trellis.shutdown
144
144
  | `staging:` | `false` | run the staging worker (installs change capture, starts backfills) here |
145
145
  | `drain_threads:` | `0` | threads applying staged changes to the targets |
146
146
  | `worker_threads:` | `2` | the threads of each Rust runtime a handle owns (its calls', and its background client's), invisible to Ruby's own sizing |
147
-
148
- The defaults run nothing in the background. Exactly one process in a fleet
149
- should set `staging: true`, and some process must run drain threads, or no
150
- definition ever reaches `:live`.
147
+ | `max_call_pool_size:` | `20` | the most connections the handle's call pool opens; the background pool is sized by `staging:` and `drain_threads:` |
148
+ | `call_pool_wait_timeout_ms:` | `30_000` | how long a call waits for a free call-pool connection before a `ConnectivityError` |
149
+
150
+ The defaults run nothing in the background. Some process in a fleet must set
151
+ `staging: true` and some process must run drain threads, or no definition ever
152
+ reaches `:live`. Any number of processes may set `staging: true`: one takes the
153
+ producer lock and leads, the rest stand by and take over when it goes away, so a
154
+ rolling deploy needs no operator step. Under a preforking Puma with
155
+ `TRELLIS_WORKER=1`, every Puma worker connects with `staging: true`, so all but
156
+ one stand by; that is harmless, and each polls the lock every 5 seconds (see
157
+ [Rolling deploys](../../docs/embedding.md#rolling-deploys)).
158
+
159
+ ### Resources
160
+
161
+ At the defaults, a handle holds up to 20 Postgres connections (its call pool,
162
+ opened as calls need them and closed after 60 seconds idle) and 3 threads. A
163
+ worker handle (`staging: true, drain_threads: N`) also owns a background pool
164
+ of up to `2N + 5` connections (`2N + 4` without staging, `1` with staging
165
+ alone), closed after 60 seconds idle, and two connections that stay open: the
166
+ staging session and one `LISTEN` shared by its drain threads. It holds 6
167
+ threads: drain threads are tasks on its runtime, not OS threads. Its memory is
168
+ bounded by row counts, not bytes: about `N` × 100,000 changed rows' images at
169
+ its peak. `max_call_pool_size:` lowers the 20 (the background pool is sized by
170
+ the handle's roles, not by an option), and `worker_threads:` sets the threads.
171
+ Every handle (each `Trellis::Instance` too) costs this again. The full
172
+ accounting is in
173
+ [`docs/embedding.md`](../../docs/embedding.md#what-a-handle-uses).
151
174
 
152
175
  ### Several instances
153
176
 
@@ -1729,9 +1729,10 @@ dependencies = [
1729
1729
 
1730
1730
  [[package]]
1731
1731
  name = "trellis"
1732
- version = "0.1.0-alpha"
1733
- source = "git+https://github.com/salesforce-misc/trellis?rev=31985aab1f7ddb22513973b0e85882c3eca9583f#31985aab1f7ddb22513973b0e85882c3eca9583f"
1732
+ version = "0.1.0-beta"
1733
+ source = "git+https://github.com/salesforce-misc/trellis?rev=1820fd7db091e540990d1efe190a687fb4dfeb59#1820fd7db091e540990d1efe190a687fb4dfeb59"
1734
1734
  dependencies = [
1735
+ "deadpool",
1735
1736
  "deadpool-postgres",
1736
1737
  "getrandom 0.4.3",
1737
1738
  "metrics",
@@ -1751,8 +1752,8 @@ dependencies = [
1751
1752
 
1752
1753
  [[package]]
1753
1754
  name = "trellis-embed"
1754
- version = "0.1.0-alpha"
1755
- source = "git+https://github.com/salesforce-misc/trellis?rev=31985aab1f7ddb22513973b0e85882c3eca9583f#31985aab1f7ddb22513973b0e85882c3eca9583f"
1755
+ version = "0.1.0-beta"
1756
+ source = "git+https://github.com/salesforce-misc/trellis?rev=1820fd7db091e540990d1efe190a687fb4dfeb59#1820fd7db091e540990d1efe190a687fb4dfeb59"
1756
1757
  dependencies = [
1757
1758
  "tracing",
1758
1759
  "trellis",
@@ -1760,7 +1761,7 @@ dependencies = [
1760
1761
 
1761
1762
  [[package]]
1762
1763
  name = "trellis_ruby"
1763
- version = "0.1.0-alpha"
1764
+ version = "0.1.0-beta"
1764
1765
  dependencies = [
1765
1766
  "magnus",
1766
1767
  "rb-sys",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "trellis_ruby"
3
- version = "0.1.0-alpha"
3
+ version = "0.1.0-beta"
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 = "31985aab1f7ddb22513973b0e85882c3eca9583f" }
32
- trellis-embed = { git = "https://github.com/salesforce-misc/trellis", rev = "31985aab1f7ddb22513973b0e85882c3eca9583f" }
31
+ trellis = { git = "https://github.com/salesforce-misc/trellis", rev = "1820fd7db091e540990d1efe190a687fb4dfeb59" }
32
+ trellis-embed = { git = "https://github.com/salesforce-misc/trellis", rev = "1820fd7db091e540990d1efe190a687fb4dfeb59" }
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
 
@@ -1,4 +1,4 @@
1
- //! Knowing whether a forked child may connect (issue #600).
1
+ //! Knowing whether a forked child may connect.
2
2
  //!
3
3
  //! `fork()` copies the whole address space but only the calling thread. The
4
4
  //! engine keeps process-wide state behind locks (the enum type-name
@@ -24,7 +24,7 @@
24
24
  //! and raises `Trellis::ForkedHandleError`, and a forked copy is never
25
25
  //! dropped either (see [`Handle`]'s `Drop`).
26
26
  //! - **A process forked while its parent had an engine running can't
27
- //! connect one of its own** (issue #600): it may have inherited a lock one
27
+ //! connect one of its own**: it may have inherited a lock one
28
28
  //! of the parent's engine threads held, which nothing in it can release.
29
29
  //! [`connect`] raises `Trellis::ForkedHandleError` instead of risking the
30
30
  //! hang. See the [`engine`] module.
@@ -331,7 +331,7 @@ impl Handle {
331
331
  }
332
332
 
333
333
  /// Pauses now the readers of every table whose capture was changed
334
- /// outside Trellis (#1181), and returns the tables.
334
+ /// outside Trellis, and returns the tables.
335
335
  fn detect_capture_disruptions(ruby: &Ruby, rb_self: &Self) -> Result<Vec<String>, Error> {
336
336
  rb_self.call(ruby, BlockingTrellis::start_detect_capture_disruptions)
337
337
  }
@@ -388,18 +388,21 @@ impl Handle {
388
388
  }
389
389
 
390
390
  /// The configuration the handle connected with, all but the connection
391
- /// string (see [`PlainConfig`]).
391
+ /// string, and its call pool's limits (see [`PlainConfig`]).
392
392
  fn config(ruby: &Ruby, rb_self: &Self) -> Result<RHash, Error> {
393
- let config = rb_self.with_engine(ruby, |trellis| PlainConfig::from(trellis.config()))?;
393
+ let config = rb_self.with_engine(ruby, |trellis| PlainConfig::from(trellis))?;
394
394
  record(
395
395
  ruby,
396
396
  [
397
397
  ("schema", ruby.into_value(config.schema)),
398
398
  ("target_schema", ruby.into_value(config.target_schema)),
399
- ("pool_max_size", ruby.into_value(config.pool_max_size)),
400
399
  (
401
- "pool_wait_timeout_ms",
402
- ruby.into_value(config.pool_wait_timeout_ms),
400
+ "max_call_pool_size",
401
+ ruby.into_value(config.max_call_pool_size),
402
+ ),
403
+ (
404
+ "call_pool_wait_timeout_ms",
405
+ ruby.into_value(config.call_pool_wait_timeout_ms),
403
406
  ),
404
407
  ],
405
408
  )
@@ -432,7 +435,11 @@ impl Handle {
432
435
  /// `lib/trellis/pg.rb`'s to document, and nothing falls back to the environment.
433
436
  ///
434
437
  /// Raises `Trellis::ForkedHandleError`, before starting anything, in a
435
- /// process forked while its parent had an engine running (issue #600).
438
+ /// process forked while its parent had an engine running.
439
+ #[allow(
440
+ clippy::too_many_arguments,
441
+ reason = "`function!` binds each Ruby argument to its own parameter"
442
+ )]
436
443
  fn connect(
437
444
  ruby: &Ruby,
438
445
  url: String,
@@ -441,6 +448,8 @@ fn connect(
441
448
  staging: bool,
442
449
  drain_threads: usize,
443
450
  worker_threads: usize,
451
+ max_call_pool_size: usize,
452
+ call_pool_wait_timeout_ms: u64,
444
453
  ) -> Result<Handle, Error> {
445
454
  engine::check().map_err(|err| forked_handle_error(ruby, forked_while_running(&err)))?;
446
455
  // Connecting can wait on the database (a staging worker installs its
@@ -460,6 +469,8 @@ fn connect(
460
469
  staging,
461
470
  drain_threads,
462
471
  worker_threads: Some(worker_threads),
472
+ max_call_pool_size,
473
+ call_pool_wait_timeout: Duration::from_millis(call_pool_wait_timeout_ms),
463
474
  ..Default::default()
464
475
  };
465
476
  // Can't fail after `check` passed (a process's answer never
@@ -1229,7 +1240,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
1229
1240
  }
1230
1241
 
1231
1242
  let native = ruby.define_module("Trellis")?.define_module("Native")?;
1232
- native.define_module_function("connect", function!(connect, 6))?;
1243
+ native.define_module_function("connect", function!(connect, 8))?;
1233
1244
  native.define_module_function("error_codes", function!(error_codes, 0))?;
1234
1245
  native.define_module_function("status_names", function!(status_names, 0))?;
1235
1246
  native.define_module_function("quarantine_states", function!(quarantine_states, 0))?;
data/lib/trellis/error.rb CHANGED
@@ -82,7 +82,7 @@ module Trellis
82
82
  #
83
83
  # Also Trellis.connect's error in a process forked while its parent had a
84
84
  # handle running, which may have inherited a lock that handle's threads
85
- # held (issue #600): shut down before forking (Puma's before_fork).
85
+ # held: shut down before forking (Puma's before_fork).
86
86
  class ForkedHandleError < ValidationError
87
87
  end
88
88
 
@@ -98,7 +98,7 @@ module Trellis
98
98
  # forked child's calls on an instance it inherited raise
99
99
  # Trellis::ForkedHandleError, and its shutdown leaves the parent's handle
100
100
  # alone and does nothing. So does Instance.connect in a child forked while
101
- # any instance of its parent was running (issue #600).
101
+ # any instance of its parent was running.
102
102
  class Instance
103
103
  extend Checks
104
104
  include Checks
@@ -111,7 +111,7 @@ module Trellis
111
111
  # Connects a new instance and returns it. Raises ForkedHandleError in
112
112
  # a process forked while its parent had an instance running
113
113
  # (connected, connecting, or not yet fully shut down): it may have
114
- # inherited a lock one of that instance's threads held (issue #600).
114
+ # inherited a lock one of that instance's threads held.
115
115
  #
116
116
  # - url: where the database is, as a libpq connection string
117
117
  # ("host=... dbname=...") or URL. Nothing is read from the environment.
@@ -121,8 +121,10 @@ module Trellis
121
121
  # - target_schema: the schema a bare target table name is created in.
122
122
  # - staging: whether this process runs the instance's staging worker,
123
123
  # which installs change capture on the source tables and starts each
124
- # new transform's backfill. Exactly one process in a fleet should,
125
- # per instance.
124
+ # new transform's backfill. Any number of processes may set it: one
125
+ # takes the instance's producer lock and leads, the rest stand by and
126
+ # take over when it goes away, so a rolling deploy needs no
127
+ # operator step.
126
128
  # - drain_threads: how many threads apply staged changes to the targets.
127
129
  # Some process must run at least one per instance, or no definition
128
130
  # reaches :live.
@@ -130,13 +132,20 @@ module Trellis
130
132
  # instance owns (its calls', and its background client's), invisible
131
133
  # to Ruby's own thread sizing, so kept small and explicit. Each
132
134
  # instance has its own; nothing is shared between instances.
135
+ # - max_call_pool_size: the most connections this instance's call pool
136
+ # (its API calls) opens at once. Connections open as calls need them
137
+ # and close after 60 seconds idle. The background client's pool is
138
+ # sized by staging and drain_threads, not by this.
139
+ # - call_pool_wait_timeout_ms: how long a call waits for a free call-pool
140
+ # connection before failing with a ConnectivityError.
133
141
  #
134
142
  # The defaults (staging: false, drain_threads: 0) run nothing in the
135
143
  # background, which is what a migration step or a console wants. The
136
144
  # staging worker reads Trellis's tables as it starts, so a staging: true
137
145
  # connect to an unmigrated schema fails: migrate first.
138
146
  def connect(url:, schema: "trellis", target_schema: "public", staging: false,
139
- drain_threads: 0, worker_threads: 2)
147
+ drain_threads: 0, worker_threads: 2, max_call_pool_size: 20,
148
+ call_pool_wait_timeout_ms: 30_000)
140
149
  string_option!(:url, url)
141
150
  string_option!(:schema, schema)
142
151
  string_option!(:target_schema, target_schema)
@@ -152,7 +161,19 @@ module Trellis
152
161
  "worker_threads must be a positive Integer, got: #{worker_threads.inspect}"
153
162
  end
154
163
 
155
- handle = Native.connect(url, schema, target_schema, staging, drain_threads, worker_threads)
164
+ unless max_call_pool_size.is_a?(Integer) && max_call_pool_size >= 1
165
+ raise ValidationError,
166
+ "max_call_pool_size must be a positive Integer, got: #{max_call_pool_size.inspect}"
167
+ end
168
+ unless call_pool_wait_timeout_ms.is_a?(Integer) &&
169
+ call_pool_wait_timeout_ms.between?(0, MAX_TIMEOUT_MS)
170
+ raise ValidationError,
171
+ "call_pool_wait_timeout_ms must be a non-negative Integer, got: " \
172
+ "#{call_pool_wait_timeout_ms.inspect}"
173
+ end
174
+
175
+ handle = Native.connect(url, schema, target_schema, staging, drain_threads, worker_threads,
176
+ max_call_pool_size, call_pool_wait_timeout_ms)
156
177
  instance = new(handle, schema)
157
178
  register(instance)
158
179
  instance
data/lib/trellis/pg.rb CHANGED
@@ -71,7 +71,7 @@ require_relative "instance"
71
71
  # calls on a handle it inherited raise Trellis::ForkedHandleError, except
72
72
  # Trellis.shutdown, which leaves the parent's handle alone and does nothing.
73
73
  # So does Trellis.connect in a child forked while its parent's handle was
74
- # running (issue #600).
74
+ # running.
75
75
  module Trellis
76
76
  @default = nil
77
77
  @lock = Mutex.new
@@ -49,7 +49,7 @@ module Trellis
49
49
  # - config.trellis.connect_on_boot = false, for a server that forks with
50
50
  # no hook to shut the handle down first (see below).
51
51
  #
52
- # A handle doesn't survive fork (issue #600). A preforking server that
52
+ # A handle doesn't survive fork. A preforking server that
53
53
  # loads the app before forking (Puma's preload_app!, on by default with
54
54
  # more than one worker) must shut the boot handle down before it forks,
55
55
  # and connect one in each worker:
@@ -10,7 +10,7 @@ module Trellis
10
10
  # appends the `INSERT`s that describe how the schema came to be. A database
11
11
  # built from the file (`db:schema:load`, `db:prepare`, the test database
12
12
  # `maintain_test_schema!` loads) then has every transform defined, with no
13
- # Trellis step to run: Trellis's worker finds the definitions unbuilt and builds them (issue #644, ADR-0010).
13
+ # Trellis step to run: Trellis's worker finds the definitions unbuilt and builds them (ADR-0010).
14
14
  #
15
15
  # Needs `config.active_record.schema_format = :sql`: a `schema.rb` can't
16
16
  # carry another schema's tables or triggers. With `:ruby`, the dump raises
@@ -156,7 +156,7 @@ module Trellis
156
156
 
157
157
  # The keywords of a libpq connection string or URI. A failure names
158
158
  # what was being read and never the string or libpq's message, which
159
- # can quote part of a password (as Trellis's own parse does, #608).
159
+ # can quote part of a password (as Trellis's own parse does).
160
160
  def conninfo(url, what)
161
161
  PG::Connection.conninfo_parse(url.to_s).to_h { |entry| [entry[:keyword], entry[:val]] }
162
162
  rescue PG::Error
@@ -376,11 +376,11 @@ module Trellis
376
376
  Divergence = Data.define(:kind, :key, :column, :persisted, :recomputed, :table, :detail)
377
377
 
378
378
  # The configuration this process's handle connected with, as Trellis.config
379
- # returns it: connect's schema and target_schema, and the connection pool's
380
- # size cap and how long a call waits for a free connection.
379
+ # returns it: connect's schema and target_schema, and the handle's call pool
380
+ # as it was built: max_call_pool_size and call_pool_wait_timeout_ms.
381
381
  #
382
382
  # Not the url: a connection string can carry a password, and a Config is
383
383
  # the kind of value that ends up in a log line whole. The caller already
384
384
  # has the url it connected with.
385
- Config = Data.define(:schema, :target_schema, :pool_max_size, :pool_wait_timeout_ms)
385
+ Config = Data.define(:schema, :target_schema, :max_call_pool_size, :call_pool_wait_timeout_ms)
386
386
  end
@@ -3,5 +3,5 @@
3
3
  module Trellis
4
4
  # The trellis-pg gem's version: the Trellis release's, which a
5
5
  # `v<VERSION>` tag releases (docs/releasing.md).
6
- VERSION = "0.1.0-alpha"
6
+ VERSION = "0.1.0-beta"
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trellis-pg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre.alpha
4
+ version: 0.1.0.pre.beta
5
5
  platform: ruby
6
6
  authors:
7
7
  - Salesforce