legion-data 1.10.8 → 1.10.9

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: 869153710ece6717285edf2de7483f4c9f5fe5b493837681b80d05b77b014971
4
- data.tar.gz: 2813fd707f0f7a80858802046b7ba2a2f23611f94a1515c66e5c7a5db1771dd6
3
+ metadata.gz: 8d929f0ed083e64145e6022ee40af61b677b852513e071ec9a986267837953a2
4
+ data.tar.gz: ed00e7cb0c7670378212cc1f8df39c893f0efbaf7a71961188a411a1956dcab9
5
5
  SHA512:
6
- metadata.gz: 41aff7bbce675a95d8a2ec6683eb0350decde2cf66854971b0c43fed1d4a09bdc50563e922bfe29d5a01660c522d579be7f2719495150d136ba262f433ba4ac4
7
- data.tar.gz: c766be4f0a13cf3ac3dc91e305d275742d5c49206ec90819631d05739137fb1c9eadbc550feeef1d6e635961ec8bf439b9b5fc19dfc334d1798859573d87902a
6
+ metadata.gz: fdf3cf290669b970b523c3de8e5d6b42b9b14d877bd7260155c2067a708745cd59120605059dc1d5af85fd94f0a63d0c1e4e39ae4b7870409357572085b1d5f2
7
+ data.tar.gz: 3b1c0bb52ba3bfcc0249f6a6a4c0185b213f7e6cd8201989f5cd0279be1d9e1df1e364c0479d48920397f7e693ff5a63f557048acdaaefecdd32651e212922c7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Legion::Data Changelog
2
2
 
3
+ ## [1.10.9] - 2026-07-31
4
+ ### Fixed
5
+ - **Postgres session timeouts**: Apply `statement_timeout` (default 5000ms) and `lock_timeout` (default 2000ms) via `after_connect` hook on every new Postgres connection. Prevents unbounded `SELECT` or lock-wait from hanging the process indefinitely — the root cause of ~1h50m connection wedges.
6
+ - **TCP keepalives on Postgres**: Wire libpq `keepalives`, `keepalives_idle` (10s), `keepalives_interval` (5s), `keepalives_count` (3), and `tcp_user_timeout` (15000ms) through the Sequel connection hash. Detects half-open TCP sockets within ~25s instead of relying on OS defaults (often 2+ hours).
7
+ - **Force-disconnect on shutdown**: `shutdown` now waits up to `shutdown_timeout` (default 5s) for checked-out connections to return, then force-disconnects all remaining connections. Previously, `disconnect` only closed idle pool connections — a connection stuck in `select()` survived forever, hanging process exit.
8
+ - **Adapter timeout defaults tightened**: Postgres `connect_timeout` reduced from 20s to 5s; MySQL `connect_timeout` reduced from 120s to 5s, `read_timeout` and `write_timeout` default to 5s. The old 20s/120s values allowed connection attempts to block worker threads far too long under network partition.
9
+ - **MySQL read/write timeout wiring preserved**: `read_timeout` and `write_timeout` remain in `ADAPTER_KEYS[:mysql2]` where Sequel's mysql2 adapter honors them. For Postgres, the equivalent is `statement_timeout` (query bound) plus TCP keepalives (socket bound) — `read_timeout` is not a libpq/Sequel-postgres concept, so it is correctly absent from the Postgres key list.
10
+
3
11
  ## [1.10.8] - 2026-07-23
4
12
  ### Fixed
5
13
  - Detect auth failures (`role does not exist`, `password authentication failed`) on Sequel pool connections and trigger immediate Vault lease reissue via LeaseManager instead of retrying dead credentials forever. 30-second cooldown prevents reissue storms during bulk credential rotation.
@@ -19,14 +19,18 @@ module Legion
19
19
 
20
20
  ADAPTER_KEYS = {
21
21
  sqlite: %i[timeout readonly disable_dqs],
22
- postgres: %i[connect_timeout sslmode sslrootcert search_path],
22
+ postgres: %i[connect_timeout sslmode sslrootcert search_path
23
+ keepalives keepalives_idle keepalives_interval keepalives_count
24
+ tcp_user_timeout],
23
25
  mysql2: %i[connect_timeout read_timeout write_timeout encoding sql_mode]
24
26
  }.freeze
25
27
 
26
28
  ADAPTER_DEFAULTS = {
27
29
  sqlite: { timeout: 5000, readonly: false, disable_dqs: true },
28
- postgres: { connect_timeout: 20, sslmode: 'disable' },
29
- mysql2: { connect_timeout: 120, encoding: 'utf8mb4' }
30
+ postgres: { connect_timeout: 5, sslmode: 'disable',
31
+ keepalives: 1, keepalives_idle: 10, keepalives_interval: 5,
32
+ keepalives_count: 3, tcp_user_timeout: 15_000 },
33
+ mysql2: { connect_timeout: 5, read_timeout: 5, write_timeout: 5, encoding: 'utf8mb4' }
30
34
  }.freeze
31
35
 
32
36
  QUERY_LOG_DIR = File.expand_path('~/.legionio/logs').freeze
@@ -272,7 +276,10 @@ module Legion
272
276
  end
273
277
 
274
278
  def shutdown
275
- @sequel&.disconnect
279
+ if @sequel
280
+ timeout = Legion::Settings[:data][:shutdown_timeout]
281
+ force_disconnect_pool(timeout: timeout)
282
+ end
276
283
  @query_file_logger&.close
277
284
  @query_file_logger = nil
278
285
  @fallback_active = false
@@ -430,6 +437,28 @@ module Legion
430
437
  data_settings[:dev_mode] == true && data_settings[:dev_fallback] != false
431
438
  end
432
439
 
440
+ def force_disconnect_pool(timeout:)
441
+ pool = @sequel.pool
442
+ in_use = pool.size - (pool.respond_to?(:available_connections) ? pool.available_connections.size : 0)
443
+
444
+ if in_use.positive?
445
+ log.warn("Legion::Data shutdown: #{in_use} connection(s) still checked out, waiting up to #{timeout}s")
446
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout
447
+ while ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) < deadline
448
+ break if pool.size <= (pool.respond_to?(:available_connections) ? pool.available_connections.size : 0)
449
+
450
+ sleep 0.1
451
+ end
452
+ remaining = pool.size - (pool.respond_to?(:available_connections) ? pool.available_connections.size : 0)
453
+ log.warn("Legion::Data shutdown: force-disconnecting #{remaining} wedged connection(s)") if remaining.positive?
454
+ end
455
+
456
+ @sequel.disconnect
457
+ rescue StandardError => e
458
+ handle_exception(e, level: :warn, handled: true, operation: :force_disconnect_pool)
459
+ @sequel&.disconnect rescue nil # rubocop:disable Style/RescueModifier
460
+ end
461
+
433
462
  def sqlite_path
434
463
  path = Legion::Settings[:data][:creds][:database] || 'legionio.db'
435
464
  return path if File.absolute_path?(path)
@@ -513,6 +542,11 @@ module Legion
513
542
  tuning[:connection_expiration] = data[:connection_expiration]
514
543
  tuning[:connection_expiration_timeout] = data[:connection_expiration_timeout]
515
544
 
545
+ # Session timeouts (postgres)
546
+ tuning[:statement_timeout] = data[:statement_timeout]
547
+ tuning[:lock_timeout] = data[:lock_timeout]
548
+ tuning[:shutdown_timeout] = data[:shutdown_timeout]
549
+
516
550
  # Adapter-specific (only keys relevant to current adapter)
517
551
  defaults = ADAPTER_DEFAULTS.fetch(adapter, {})
518
552
  ADAPTER_KEYS.fetch(adapter, []).each do |key|
@@ -621,6 +655,7 @@ module Legion
621
655
  if adapter == :postgres
622
656
  Sequel.extension(:pg_array)
623
657
  @sequel.extension(:pg_array)
658
+ install_postgres_session_timeouts
624
659
  end
625
660
 
626
661
  if data[:connection_validation] != false
@@ -638,6 +673,21 @@ module Legion
638
673
  handle_exception(e, level: :warn, handled: true, operation: :configure_extensions, adapter: adapter)
639
674
  end
640
675
 
676
+ def install_postgres_session_timeouts
677
+ data = Legion::Settings[:data]
678
+ stmt_timeout = data[:statement_timeout]
679
+ lck_timeout = data[:lock_timeout]
680
+
681
+ @sequel.pool.after_connect = proc do |conn|
682
+ conn.exec("SET statement_timeout = '#{stmt_timeout.to_i}ms'") if stmt_timeout
683
+ conn.exec("SET lock_timeout = '#{lck_timeout.to_i}ms'") if lck_timeout
684
+ end
685
+
686
+ log.info("Postgres session timeouts: statement_timeout=#{stmt_timeout}ms, lock_timeout=#{lck_timeout}ms")
687
+ rescue StandardError => e
688
+ handle_exception(e, level: :warn, handled: true, operation: :install_postgres_session_timeouts)
689
+ end
690
+
641
691
  def install_auth_failure_hook
642
692
  Legion::Data::AuthFailureHandler.install(@sequel)
643
693
  rescue StandardError => e
@@ -58,6 +58,28 @@ module Legion
58
58
  connection_expiration: true,
59
59
  connection_expiration_timeout: 14_400,
60
60
 
61
+ # Postgres session timeouts (milliseconds, applied via SET on each new connection).
62
+ # statement_timeout bounds any single query; lock_timeout bounds lock acquisition.
63
+ # These prevent a wedged SELECT or lock wait from hanging the process indefinitely.
64
+ statement_timeout: 5000,
65
+ lock_timeout: 2000,
66
+
67
+ # TCP keepalives for Postgres (libpq connection params).
68
+ # Detect half-open sockets where the peer vanished without FIN/RST.
69
+ # keepalives_idle: seconds before first keepalive probe after idle
70
+ # keepalives_interval: seconds between probes
71
+ # keepalives_count: failed probes before declaring connection dead
72
+ # tcp_user_timeout: total ms for unacked data before kernel drops connection (Linux)
73
+ keepalives: 1,
74
+ keepalives_idle: 10,
75
+ keepalives_interval: 5,
76
+ keepalives_count: 3,
77
+ tcp_user_timeout: 15_000,
78
+
79
+ # Shutdown: max seconds to wait for checked-out connections to return before
80
+ # force-disconnecting them.
81
+ shutdown_timeout: 5,
82
+
61
83
  # Adapter-specific (nil = use adapter built-in default)
62
84
  connect_timeout: nil,
63
85
  read_timeout: nil,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Data
5
- VERSION = '1.10.8'
5
+ VERSION = '1.10.9'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-data
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.8
4
+ version: 1.10.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity