standard_ledger 0.6.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f6aa5d56ba73fe170790d016b50f00aec95f563346a08c36f8823d05fbddd98
4
- data.tar.gz: dc1ef4c96e69b6a8d4832ce1f80dc6a979d63dc25ff059ed77794b829f414e7f
3
+ metadata.gz: 764772bfbc5d54165460fe4bcb75c3c1c708136ac0a5c20face111aeb866298c
4
+ data.tar.gz: a5f692a20fa2fc0d800249276325e8fe99e4607b34047f3c6ebaf87ab929f01d
5
5
  SHA512:
6
- metadata.gz: 54f927af7564ee7e9319a2fd87358a86e91ab55c7d3fea872dc9f821efc40cde0cd31b55cdf4b90e7bfb138bb501a7e9888358a2645f27c662058ba2fd2ef869
7
- data.tar.gz: 108ce4c242c054512e6f3f173cf683dc9f71ee08882ed82dd505be092af6fb08f7b97b635aa5685ed5c08f7257e6ad285d327a2b4a9bd1f07c2ee1bcfcda19d2
6
+ metadata.gz: a4da13dc9f29813caa213aa6693d2c960a6506fabe32faf4d29b67a872b00ef0daa4d1c3efd8c77061b795c2dee253dbf9e3305a81b3b5c4ca454155450f0342
7
+ data.tar.gz: ce26ae4bf9b159ee039d997cca604fb01a4b87f96ee0874f9a54bdb7167bcb898b38a11e52651b8951147d5eea55d91d210c3bff394d55b6cb31987cf6f23158
data/CHANGELOG.md CHANGED
@@ -6,6 +6,72 @@ project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.0] - 2026-10-28
10
+
11
+ ### Fixed
12
+
13
+ - **An idempotent replay inside a host transaction no longer poisons it on
14
+ Postgres.** `create!` now inserts inside a savepoint
15
+ (`transaction(requires_new: true)`), so a duplicate idempotency key rolls back
16
+ only that INSERT. Before, the failed INSERT aborted the host's whole
17
+ transaction and the rescue's `find_by` raised `PG::InFailedSqlTransaction`,
18
+ as did every later statement in it. Covered by a deterministic duplicate and
19
+ a two-connection race on the new Postgres CI leg.
20
+
21
+ ### Changed
22
+
23
+ - **The suite also runs on PostgreSQL in CI** (`test (postgres)`). Set
24
+ `DATABASE_URL` to a database whose name contains `test` to run it locally;
25
+ the dummy schema is loaded with `force: true`.
26
+
27
+ - **Requires Rails 8.1** (`railties`, `activerecord`, `activesupport` `>= 8.1`,
28
+ was `>= 8.0`). Every consumer app runs 8.1; 8.0 was never exercised in CI.
29
+
30
+ ## [0.7.0] - 2026-09-24
31
+
32
+ The Phase 4 release. The removals the cleanup plan scheduled for it (the
33
+ `scheduler` config, `PartialFailure`, and the engine's no-op initializer)
34
+ already shipped in 0.6.0, and 0.6.0 deprecated nothing, so this release only
35
+ changes how `refresh!` reports failures. It is a minor bump because hosts that
36
+ report refresh failures themselves should delete that code.
37
+
38
+ ### Changed
39
+ - **`StandardLedger.refresh!` reports SQL failures through `Rails.error`.**
40
+ When the `REFRESH` statement raises, the gem emits `projection.failed` (as
41
+ before), then calls `Rails.error.report(error, handled: false,
42
+ severity: :error, context: { view:, concurrently: }, source:
43
+ "standard_ledger")`, then re-raises. It falls back to
44
+ `ActiveSupport.error_reporter` outside Rails. A reporter that raises is
45
+ ignored, so it can never mask the refresh error. Input errors raised before
46
+ any SQL runs are not reported (`ArgumentError`, `RefreshInsideTransaction`).
47
+ **No double reports:** Rails' `ErrorReporter` flags a reported exception
48
+ and skips it when the same object is reported again. That covers a host
49
+ rescue that reports and re-raises, and the executor wrapping the job.
50
+ - The `:auto` catalog-probe failure report (handled, `:warning`) now goes
51
+ through the same `StandardLedger.report_error` helper (`@api private`).
52
+ Its payload is unchanged.
53
+
54
+ ### Upgrade notes (0.6.x → 0.7.0)
55
+ 1. Bump to `gem "standard_ledger", "~> 0.7"`. Nothing was removed, so no code
56
+ change is required.
57
+ 2. **Optional cleanup:** delete any report-and-re-raise rescue around
58
+ `refresh!`. Grepped `origin/main` of all consumers on 2026-09-24:
59
+ - luminality-web `app/jobs/refresh_admin_dashboard_metrics_job.rb:20-27`
60
+ (`rescue StandardError => e; Rails.error.report(e, handled: true,
61
+ context: { view: "admin_dashboard_metrics" }); raise`). Delete the rescue
62
+ and fix the header comment ("…does not report").
63
+ - sidekick-web `app/jobs/refresh_materialized_views_job.rb` has no rescue.
64
+ Its refresh failures are now reported by the gem, with view context,
65
+ before the job runner sees them.
66
+ - jumpdrive-web and fundbright-web don't call `refresh!`.
67
+ 3. **Expect a different Sentry grouping context** for refresh failures:
68
+ `source: "standard_ledger"` and `context.view`. The error class and
69
+ backtrace are unchanged.
70
+ 4. Sorbet apps: regenerate the gem RBI (`bin/tapioca gem standard_ledger`).
71
+ jumpdrive-web's `control-plane/config/initializers/standard_ledger.rb:12-13`
72
+ still mentions the removed `scheduler`/`default_async_*` config in a
73
+ comment. The comment is historical and harmless.
74
+
9
75
  ## [0.6.0] - 2026-09-24
10
76
 
11
77
  A breaking release. The declarative projection engine is removed, and the gem
data/README.md CHANGED
@@ -129,14 +129,21 @@ with `concurrently: false` can pass `concurrently: :auto` instead.
129
129
 
130
130
  `refresh!` returns a success Result with
131
131
  `projections[:refreshed] = [{ view:, concurrently: }]` (the concurrency mode
132
- that was actually used). If the SQL fails, it emits `projection.failed` and
133
- re-raises so your job runner can retry. View names must be bare or
134
- `schema.view` identifiers. Anything else raises `ArgumentError`.
132
+ that was actually used). If the SQL fails, it emits `projection.failed`,
133
+ reports the error to `Rails.error` (`handled: false`, `severity: :error`,
134
+ `context: { view:, concurrently: }`, `source: "standard_ledger"`), and
135
+ re-raises so your job runner can retry. Since 0.7 you don't need a
136
+ report-and-re-raise rescue around `refresh!` in your job. An existing one is
137
+ harmless: Rails skips an exception object it has already reported, so the
138
+ failure is reported once. Input errors (an invalid view name, an unknown
139
+ `concurrently:`, `RefreshInsideTransaction`) are raised before any SQL runs
140
+ and are not reported by the gem. View names must be bare or `schema.view`
141
+ identifiers. Anything else raises `ArgumentError`.
135
142
 
136
143
  ## Installation
137
144
 
138
145
  ```ruby
139
- gem "standard_ledger", "~> 0.6"
146
+ gem "standard_ledger", "~> 0.7"
140
147
  ```
141
148
 
142
149
  (Don't reintroduce a `git:` reference. It makes a bare `bundle install` a
@@ -80,7 +80,13 @@ module StandardLedger
80
80
 
81
81
  validate_standard_ledger_idempotency_index!
82
82
 
83
- super
83
+ # A savepoint, so a duplicate rolls back only this INSERT. Hosts post
84
+ # inside their own transaction, and on Postgres a failed statement
85
+ # aborts the whole transaction: without it the find_by below, and
86
+ # every later statement in the host's transaction, raises
87
+ # PG::InFailedSqlTransaction. With no outer transaction this is the
88
+ # same single transaction create! would open anyway.
89
+ transaction(requires_new: true) { super(attributes, &block) }
84
90
  rescue ActiveRecord::RecordNotUnique => e
85
91
  raise unless standard_ledger_idempotency_violation?(e)
86
92
 
@@ -44,8 +44,15 @@ module StandardLedger
44
44
  end
45
45
 
46
46
  # Issue `REFRESH MATERIALIZED VIEW [CONCURRENTLY] <view_name>` and emit
47
- # `<prefix>.projection.refreshed` on success, or
48
- # `<prefix>.projection.failed` before re-raising on SQL failure.
47
+ # `<prefix>.projection.refreshed` on success. On SQL failure, emit
48
+ # `<prefix>.projection.failed`, report the error through `Rails.error`
49
+ # (`handled: false`, `severity: :error`) and re-raise.
50
+ #
51
+ # Reporting before the re-raise means host jobs no longer need their
52
+ # own report-and-re-raise rescue. It cannot double-report: Rails'
53
+ # ErrorReporter flags a reported exception and skips it when the same
54
+ # object is reported again (by a host rescue, or by the executor that
55
+ # wraps the job).
49
56
  #
50
57
  # The view name is validated against a SQL-identifier regex at the
51
58
  # boundary as defence in depth — a careless host could pass through a
@@ -72,6 +79,10 @@ module StandardLedger
72
79
  "#{prefix}.projection.failed",
73
80
  view: view_name.to_s, concurrently: concurrently, mode: :matview, error: e
74
81
  )
82
+ StandardLedger.report_error(
83
+ e, handled: false, severity: :error,
84
+ context: { view: view_name.to_s, concurrently: concurrently }
85
+ )
75
86
  raise
76
87
  end
77
88
 
@@ -140,23 +151,13 @@ module StandardLedger
140
151
 
141
152
  boolean(row["populated"]) && boolean(row["unique_index"])
142
153
  rescue StandardError => e
143
- report_error(e, view: view_name.to_s)
154
+ StandardLedger.report_error(e, handled: true, severity: :warning, context: { view: view_name.to_s })
144
155
  false
145
156
  end
146
157
 
147
158
  def boolean(value)
148
159
  ActiveModel::Type::Boolean.new.cast(value) == true
149
160
  end
150
-
151
- def report_error(error, context)
152
- reporter =
153
- if defined?(::Rails) && ::Rails.respond_to?(:error)
154
- ::Rails.error
155
- elsif ActiveSupport.respond_to?(:error_reporter)
156
- ActiveSupport.error_reporter
157
- end
158
- reporter&.report(error, handled: true, severity: :warning, context: context, source: "standard_ledger")
159
- end
160
161
  end
161
162
  end
162
163
  end
@@ -1,3 +1,3 @@
1
1
  module StandardLedger
2
- VERSION = "0.6.0"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -92,9 +92,16 @@ module StandardLedger
92
92
 
93
93
  # Refresh a host-owned materialized view. Issues
94
94
  # `REFRESH MATERIALIZED VIEW [CONCURRENTLY] <view_name>` against the
95
- # active connection and emits `<prefix>.projection.refreshed` on success
96
- # (or `<prefix>.projection.failed` on raise, before re-raising — the
97
- # host's job runner needs to see the failure to drive its retry path).
95
+ # active connection and emits `<prefix>.projection.refreshed` on success.
96
+ # When the refresh SQL raises, it emits `<prefix>.projection.failed`,
97
+ # reports the error through `Rails.error` (`handled: false`,
98
+ # `severity: :error`, `context: { view:, concurrently: }`,
99
+ # `source: "standard_ledger"`) and re-raises, so the host's job runner
100
+ # still drives its retry path. Hosts don't need their own
101
+ # report-and-re-raise; Rails skips an exception it has already reported,
102
+ # so an existing one is harmless. Argument errors and
103
+ # `RefreshInsideTransaction` are programming errors raised before any
104
+ # SQL runs; they are not reported by the gem.
98
105
  #
99
106
  # @example scheduled job refreshing a list of views
100
107
  # VIEWS.each { |view| StandardLedger.refresh!(view, concurrently: :auto) }
@@ -130,6 +137,23 @@ module StandardLedger
130
137
  )
131
138
  end
132
139
 
140
+ # Report +error+ through `Rails.error` (or `ActiveSupport.error_reporter`
141
+ # outside Rails) with `source: "standard_ledger"`. A reporter failure is
142
+ # swallowed so it can never mask the error being reported.
143
+ #
144
+ # @api private
145
+ def report_error(error, handled:, severity:, context:)
146
+ reporter =
147
+ if defined?(::Rails) && ::Rails.respond_to?(:error)
148
+ ::Rails.error
149
+ elsif ActiveSupport.respond_to?(:error_reporter)
150
+ ActiveSupport.error_reporter
151
+ end
152
+ reporter&.report(error, handled: handled, severity: severity, context: context, source: "standard_ledger")
153
+ rescue StandardError
154
+ nil
155
+ end
156
+
133
157
  private
134
158
 
135
159
  # Resolve the kind column name for an entry class. Falls back to `:kind`
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_ledger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim
@@ -15,42 +15,42 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '8.0'
18
+ version: '8.1'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '8.0'
25
+ version: '8.1'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: activerecord
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '8.0'
32
+ version: '8.1'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '8.0'
39
+ version: '8.1'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: activesupport
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '8.0'
46
+ version: '8.1'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: '8.0'
53
+ version: '8.1'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: brakeman
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -143,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
143
  - !ruby/object:Gem::Version
144
144
  version: '0'
145
145
  requirements: []
146
- rubygems_version: 4.0.3
146
+ rubygems_version: 4.0.10
147
147
  specification_version: 4
148
148
  summary: Immutable, append-only journal entries and materialized-view refresh for
149
149
  Rails apps.