transaction_guard 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 81a99a71dbadc26e6a02101a9c4200d9ebd4a79b6417caf62613005ab9872a33
4
+ data.tar.gz: 940ff5a7174f7d5b6919dbac739a7d6e0e9d488987197e584f78318acf0f5b8a
5
+ SHA512:
6
+ metadata.gz: 9ec48718d32eac1488296881b2e2f0c2acee3ebdb2002ba2f616871e4dfb0b30ecbfda144c9e964e636d6577c11c91c54e0b051f1711b964ff7f58dac0ff997e
7
+ data.tar.gz: '092340c5d26c0b1d7fb388569afcfcf7740bc17cad3debbccf247805c7b9ab72088575160f5b0faac2600eaa684e149dd2f112645126d002138d46f5cccb0c1e'
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,20 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+ Exclude:
6
+ - "vendor/**/*"
7
+ - "tmp/**/*"
8
+ - "bin/setup"
9
+ - "sig/**/*"
10
+
11
+ Metrics/BlockLength:
12
+ Exclude:
13
+ - "spec/**/*"
14
+ - "*.gemspec"
15
+
16
+ Style/StringLiterals:
17
+ EnforcedStyle: double_quotes
18
+
19
+ Style/StringLiteralsInInterpolation:
20
+ EnforcedStyle: double_quotes
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-09-18
4
+
5
+ ### Added
6
+
7
+ - Detect `Net::HTTP` requests inside ActiveRecord transactions
8
+ - Detect ActionMailer `deliver_now` / `deliver_later` inside transactions
9
+ - Detect ActiveJob `perform_later` / `perform_now` inside transactions
10
+ - Configuration modes: `:warn` (default), `:raise`, and `:off`
11
+ - Rails Railtie with development/test `:warn` and production `:off` defaults
12
+ - Caller location in warning and error messages
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ yashikavijay2799@gmail.com.
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,60 @@
1
+ # Contributing to TransactionGuard
2
+
3
+ Thanks for helping improve TransactionGuard.
4
+
5
+ ## How to contribute
6
+
7
+ `master` is protected. Direct pushes are not accepted.
8
+
9
+ 1. **Fork** the repository on GitHub.
10
+ 2. **Clone your fork** and create a branch from `master`.
11
+ 3. Make your changes, then open a **pull request** against `master` on
12
+ [yashika279/transaction_guard](https://github.com/yashika279/transaction_guard).
13
+
14
+ ```bash
15
+ git clone https://github.com/<your-username>/transaction_guard.git
16
+ cd transaction_guard
17
+ git remote add upstream https://github.com/yashika279/transaction_guard.git
18
+ git checkout -b my-change
19
+ bundle install
20
+ ```
21
+
22
+ Keep your branch up to date before opening the PR:
23
+
24
+ ```bash
25
+ git fetch upstream
26
+ git rebase upstream/master
27
+ ```
28
+
29
+ ## Feedback and issues
30
+
31
+ Use [GitHub Issues](https://github.com/yashika279/transaction_guard/issues) for:
32
+
33
+ * Bug reports
34
+ * Feature ideas
35
+ * Questions and general feedback
36
+
37
+ Prefer the issue templates when they fit. Include:
38
+
39
+ 1. Ruby / Rails / ActiveRecord versions
40
+ 2. TransactionGuard mode (`:warn`, `:raise`, or `:off`)
41
+ 3. A minimal reproduction if possible
42
+
43
+ Security vulnerabilities should be reported privately — see [SECURITY.md](SECURITY.md).
44
+
45
+ ## Checks before opening a PR
46
+
47
+ ```bash
48
+ bundle exec rspec
49
+ bundle exec rubocop
50
+ ```
51
+
52
+ CI must pass on your pull request before it can be merged.
53
+
54
+ ## Guidelines
55
+
56
+ - Keep the public API small and focused on detecting side effects inside ActiveRecord transactions.
57
+ - Prefer optional integrations (load when the library is present) over hard runtime dependencies.
58
+ - Add or update specs for behavior changes.
59
+ - Update `CHANGELOG.md` under `[Unreleased]` when relevant.
60
+ - Be respectful and follow the [Code of Conduct](CODE_OF_CONDUCT.md).
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yashika
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,278 @@
1
+ # TransactionGuard
2
+
3
+ TransactionGuard detects external side effects performed inside ActiveRecord transactions.
4
+
5
+ Database transactions can roll back database changes, but they cannot automatically roll back operations such as:
6
+
7
+ * HTTP requests
8
+ * Email delivery
9
+ * Background job enqueueing
10
+
11
+ For example:
12
+
13
+ ```ruby
14
+ User.transaction do
15
+ user = User.create!(name: "Yashika")
16
+
17
+ SomeExternalApi.create_user(user)
18
+ end
19
+ ```
20
+
21
+ If the transaction later rolls back, the external API request cannot automatically be rolled back with it.
22
+
23
+ TransactionGuard helps identify these situations during development and testing.
24
+
25
+ ## Installation
26
+
27
+ Add the gem to your application's Gemfile:
28
+
29
+ ```ruby
30
+ gem "transaction_guard"
31
+ ```
32
+
33
+ Then run:
34
+
35
+ ```bash
36
+ bundle install
37
+ ```
38
+
39
+ For local development, you can use the gem directly from a local path:
40
+
41
+ ```ruby
42
+ gem "transaction_guard", path: "../transaction_guard"
43
+ ```
44
+
45
+ ## Configuration
46
+
47
+ TransactionGuard supports three modes:
48
+
49
+ * `:warn` — report external side effects
50
+ * `:raise` — raise an error when a side effect is detected
51
+ * `:off` — disable detection
52
+
53
+ The default mode is `:warn`.
54
+
55
+ In Rails applications, the included Railtie sets:
56
+
57
+ * `:warn` in development and test
58
+ * `:off` in production
59
+
60
+ Override this in an initializer:
61
+
62
+ ```ruby
63
+ # config/initializers/transaction_guard.rb
64
+ TransactionGuard.configure do |config|
65
+ config.mode = :warn
66
+ end
67
+ ```
68
+
69
+ ### Warn mode
70
+
71
+ This is the default:
72
+
73
+ ```ruby
74
+ TransactionGuard.configure do |config|
75
+ config.mode = :warn
76
+ end
77
+ ```
78
+
79
+ When an external side effect is detected inside a transaction, TransactionGuard reports a warning including the operation and caller location.
80
+
81
+ ### Raise mode
82
+
83
+ Use `:raise` when you want to prevent the transaction from continuing after an external side effect is detected:
84
+
85
+ ```ruby
86
+ TransactionGuard.configure do |config|
87
+ config.mode = :raise
88
+ end
89
+ ```
90
+
91
+ An invalid configuration value raises an `ArgumentError`:
92
+
93
+ ```ruby
94
+ TransactionGuard.configure do |config|
95
+ config.mode = :invalid
96
+ end
97
+ ```
98
+
99
+ ### Off mode
100
+
101
+ Detection can be disabled:
102
+
103
+ ```ruby
104
+ TransactionGuard.configure do |config|
105
+ config.mode = :off
106
+ end
107
+ ```
108
+
109
+ ## HTTP detection
110
+
111
+ TransactionGuard detects HTTP requests made through `Net::HTTP` while an ActiveRecord transaction is open.
112
+
113
+ Example:
114
+
115
+ ```ruby
116
+ User.transaction do
117
+ Net::HTTP.get(URI("https://example.com"))
118
+ end
119
+ ```
120
+
121
+ TransactionGuard reports the external HTTP operation.
122
+
123
+ The detector covers common `Net::HTTP` methods including:
124
+
125
+ ```ruby
126
+ get
127
+ post
128
+ put
129
+ patch
130
+ delete
131
+ head
132
+ options
133
+ ```
134
+
135
+ Clients that build on `Net::HTTP` (for example some Faraday adapters) may also be detected. Direct Faraday, HTTParty, httpx, and similar clients are not hooked in 0.1.0.
136
+
137
+ ## Email detection
138
+
139
+ TransactionGuard detects email delivery performed inside an ActiveRecord transaction.
140
+
141
+ For example:
142
+
143
+ ```ruby
144
+ User.transaction do
145
+ user = User.create!
146
+
147
+ TestMailer.welcome(user).deliver_now
148
+ end
149
+ ```
150
+
151
+ It also detects:
152
+
153
+ ```ruby
154
+ User.transaction do
155
+ TestMailer.welcome(user).deliver_later
156
+ end
157
+ ```
158
+
159
+ `deliver_later` is reported as an email side effect rather than generating an additional warning for the internal ActiveJob enqueue.
160
+
161
+ ## Background job detection
162
+
163
+ TransactionGuard detects ActiveJob operations performed inside transactions.
164
+
165
+ ### Enqueueing a job
166
+
167
+ ```ruby
168
+ User.transaction do
169
+ user = User.create!
170
+
171
+ WelcomeJob.perform_later(user.id)
172
+ end
173
+ ```
174
+
175
+ This is reported as a job enqueue operation.
176
+
177
+ ### Executing a job immediately
178
+
179
+ ```ruby
180
+ User.transaction do
181
+ WelcomeJob.perform_now
182
+ end
183
+ ```
184
+
185
+ This is reported as job execution.
186
+
187
+ Sidekiq, Resque, and other non-ActiveJob APIs are not detected in 0.1.0.
188
+
189
+ ## Why does this matter?
190
+
191
+ Consider:
192
+
193
+ ```ruby
194
+ User.transaction do
195
+ user = User.create!
196
+
197
+ WelcomeJob.perform_later(user.id)
198
+
199
+ raise ActiveRecord::Rollback
200
+ end
201
+ ```
202
+
203
+ The database record is rolled back, but the background job may already have been enqueued.
204
+
205
+ The job could therefore execute with an ID that no longer exists.
206
+
207
+ Similar problems can occur with HTTP requests and email delivery.
208
+
209
+ ## Recommended alternatives
210
+
211
+ When an external side effect depends on a successful database transaction, consider moving the operation until after the transaction commits.
212
+
213
+ For example:
214
+
215
+ ```ruby
216
+ user = User.create!
217
+
218
+ User.transaction do
219
+ user.update!(status: "active")
220
+ end
221
+
222
+ WelcomeJob.perform_later(user.id)
223
+ ```
224
+
225
+ For more complex workflows, consider patterns such as:
226
+
227
+ * `after_commit`
228
+ * ActiveJob triggered after a successful commit
229
+ * transactional outbox
230
+ * reliable event publishing
231
+
232
+ TransactionGuard does not automatically move, delay, retry, or otherwise modify external operations. It reports the potentially unsafe operation so the application can decide how to handle it.
233
+
234
+ ## Development
235
+
236
+ Clone the repository and install dependencies:
237
+
238
+ ```bash
239
+ git clone https://github.com/yashika279/transaction_guard.git
240
+ cd transaction_guard
241
+ bundle install
242
+ ```
243
+
244
+ Run the test suite:
245
+
246
+ ```bash
247
+ bundle exec rspec
248
+ ```
249
+
250
+ Run RuboCop:
251
+
252
+ ```bash
253
+ bundle exec rubocop
254
+ ```
255
+
256
+ Build the gem locally:
257
+
258
+ ```bash
259
+ bundle exec gem build transaction_guard.gemspec
260
+ ```
261
+
262
+ ## Contributing
263
+
264
+ Bug reports, feature ideas, feedback, and pull requests are welcome.
265
+
266
+ - Open an [issue](https://github.com/yashika279/transaction_guard/issues/new/choose) for bugs, features, or feedback.
267
+ - `master` is protected — **fork the repo**, push your branch, and open a PR against `master`.
268
+
269
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, the fork workflow, and the PR checklist.
270
+ Please make sure tests and RuboCop pass before submitting a pull request.
271
+
272
+ ## Security
273
+
274
+ See [SECURITY.md](SECURITY.md) for how to report vulnerabilities.
275
+
276
+ ## License
277
+
278
+ TransactionGuard is available as open source under the MIT License.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/SECURITY.md ADDED
@@ -0,0 +1,19 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes are applied to the latest released version of TransactionGuard.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Please report security issues privately by emailing **yashikavijay2799@gmail.com**.
10
+
11
+ Do not open a public GitHub issue for vulnerabilities until a fix is available.
12
+
13
+ Include:
14
+
15
+ - A description of the issue
16
+ - Steps to reproduce
17
+ - Impact assessment if known
18
+
19
+ You should receive an acknowledgment within a few days.
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TransactionGuard
4
+ # Stores TransactionGuard configuration.
5
+ class Configuration
6
+ MODES = %i[off warn raise].freeze
7
+
8
+ attr_reader :mode
9
+
10
+ def initialize
11
+ @mode = :warn
12
+ end
13
+
14
+ def mode=(value)
15
+ raise ArgumentError, "Invalid mode: #{value.inspect}" unless MODES.include?(value)
16
+
17
+ @mode = value
18
+ end
19
+
20
+ def enabled?
21
+ mode != :off
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TransactionGuard
4
+ module Detectors
5
+ # Detects HTTP requests made inside ActiveRecord transactions.
6
+ module HTTP
7
+ HTTP_METHODS = %i[
8
+ delete
9
+ get
10
+ head
11
+ options
12
+ patch
13
+ post
14
+ put
15
+ ].freeze
16
+
17
+ def request(...)
18
+ report_http_request("HTTP request") unless http_request_in_progress?
19
+
20
+ with_http_request { super }
21
+ end
22
+
23
+ HTTP_METHODS.each do |method|
24
+ define_method(method) do |*args, **kwargs, &block|
25
+ report_http_request("HTTP #{method.upcase}") unless http_request_in_progress?
26
+
27
+ with_http_request do
28
+ super(*args, **kwargs, &block)
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def report_http_request(operation)
36
+ return unless TransactionGuard.configuration.enabled?
37
+ return unless TransactionGuard::Transaction.open?
38
+
39
+ TransactionGuard::Reporter.report(operation: operation)
40
+ end
41
+
42
+ def http_request_in_progress?
43
+ Thread.current[:transaction_guard_http_request]
44
+ end
45
+
46
+ def with_http_request
47
+ previous_state = Thread.current[:transaction_guard_http_request]
48
+ Thread.current[:transaction_guard_http_request] = true
49
+
50
+ yield
51
+ ensure
52
+ Thread.current[:transaction_guard_http_request] = previous_state
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TransactionGuard
4
+ module Detectors
5
+ # Detects background job operations inside ActiveRecord transactions.
6
+ module Job
7
+ def perform_later(...)
8
+ report_job("Job enqueue")
9
+ super
10
+ end
11
+
12
+ def perform_now(...)
13
+ report_job("Job execution")
14
+ super
15
+ end
16
+
17
+ private
18
+
19
+ def report_job(operation)
20
+ return if mail_delivery_in_progress?
21
+ return unless TransactionGuard.configuration.enabled?
22
+ return unless TransactionGuard::Transaction.open?
23
+
24
+ TransactionGuard::Reporter.report(operation: operation)
25
+ end
26
+
27
+ def mail_delivery_in_progress?
28
+ Thread.current[:transaction_guard_mail_delivery]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TransactionGuard
4
+ module Detectors
5
+ # Detects email delivery inside ActiveRecord transactions.
6
+ module Mail
7
+ def deliver_now(...)
8
+ report_email unless mail_delivery_in_progress?
9
+
10
+ with_mail_delivery { super }
11
+ end
12
+
13
+ def deliver_later(...)
14
+ report_email unless mail_delivery_in_progress?
15
+
16
+ with_mail_delivery { super }
17
+ end
18
+
19
+ private
20
+
21
+ def report_email
22
+ return unless TransactionGuard.configuration.enabled?
23
+ return unless TransactionGuard::Transaction.open?
24
+
25
+ TransactionGuard::Reporter.report(operation: "Email delivery")
26
+ end
27
+
28
+ def mail_delivery_in_progress?
29
+ Thread.current[:transaction_guard_mail_delivery]
30
+ end
31
+
32
+ def with_mail_delivery
33
+ previous_state = Thread.current[:transaction_guard_mail_delivery]
34
+ Thread.current[:transaction_guard_mail_delivery] = true
35
+
36
+ yield
37
+ ensure
38
+ Thread.current[:transaction_guard_mail_delivery] = previous_state
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module TransactionGuard
6
+ # Loads TransactionGuard in Rails applications and applies sensible defaults.
7
+ class Railtie < Rails::Railtie
8
+ initializer "transaction_guard.configure" do
9
+ TransactionGuard.configure do |config|
10
+ config.mode = Rails.env.production? ? :off : :warn
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TransactionGuard
4
+ # Reports detected external side effects.
5
+ module Reporter
6
+ module_function
7
+
8
+ def report(operation:)
9
+ message = build_message(operation)
10
+
11
+ case TransactionGuard.configuration.mode
12
+ when :warn
13
+ warn message
14
+ when :raise
15
+ raise TransactionGuard::Error, message
16
+ when :off
17
+ nil
18
+ end
19
+ end
20
+
21
+ def build_message(operation)
22
+ location = caller_location
23
+ details = ["Operation: #{operation}"]
24
+ details << "Location: #{location}" if location
25
+
26
+ <<~MESSAGE
27
+ ⚠ TransactionGuard
28
+ External side effect detected inside an ActiveRecord transaction.
29
+ #{details.join("\n")}
30
+ Risk: The external operation cannot be rolled back automatically if the DB transaction later rolls back.
31
+ Consider: after_commit, ActiveJob after commit, transactional outbox
32
+ MESSAGE
33
+ end
34
+ private_class_method :build_message
35
+
36
+ def caller_location
37
+ caller_locations(1, 40)&.find do |frame|
38
+ path = frame.absolute_path || frame.path
39
+ # Skip gem internals only — not the project path (e.g. .../transaction_guard/spec/...)
40
+ path && !path.include?("/lib/transaction_guard/")
41
+ end&.to_s
42
+ end
43
+ private_class_method :caller_location
44
+ end
45
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TransactionGuard
4
+ # Provides ActiveRecord transaction state detection.
5
+ module Transaction
6
+ module_function
7
+
8
+ def open?
9
+ ActiveRecord::Base.current_transaction.open?
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TransactionGuard
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "net/http"
5
+
6
+ begin
7
+ require "action_mailer"
8
+ rescue LoadError
9
+ # ActionMailer is optional.
10
+ end
11
+
12
+ begin
13
+ require "active_job"
14
+ rescue LoadError
15
+ # ActiveJob is optional.
16
+ end
17
+
18
+ require_relative "transaction_guard/configuration"
19
+ require_relative "transaction_guard/version"
20
+ require_relative "transaction_guard/transaction"
21
+ require_relative "transaction_guard/detectors/http"
22
+ require_relative "transaction_guard/reporter"
23
+ require_relative "transaction_guard/detectors/mail"
24
+ require_relative "transaction_guard/detectors/job"
25
+
26
+ # Detects external side effects performed inside ActiveRecord transactions.
27
+ module TransactionGuard
28
+ class Error < StandardError; end
29
+
30
+ class << self
31
+ def configuration
32
+ @configuration ||= Configuration.new
33
+ end
34
+
35
+ def configure
36
+ yield(configuration)
37
+ end
38
+ end
39
+ end
40
+
41
+ Net::HTTP.prepend(TransactionGuard::Detectors::HTTP)
42
+
43
+ ActionMailer::MessageDelivery.prepend(TransactionGuard::Detectors::Mail) if defined?(ActionMailer::MessageDelivery)
44
+
45
+ ActiveJob::Base.singleton_class.prepend(TransactionGuard::Detectors::Job) if defined?(ActiveJob::Base)
46
+
47
+ require_relative "transaction_guard/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,4 @@
1
+ module TransactionGuard
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: transaction_guard
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yashika
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activerecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ description: |
27
+ Detects external side effects such as HTTP requests, email delivery, and
28
+ background job enqueuing performed inside ActiveRecord transactions,
29
+ helping prevent inconsistent state when a database transaction rolls back.
30
+ email:
31
+ - yashikavijay2799@gmail.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - ".rspec"
37
+ - ".rubocop.yml"
38
+ - CHANGELOG.md
39
+ - CODE_OF_CONDUCT.md
40
+ - CONTRIBUTING.md
41
+ - LICENSE.txt
42
+ - README.md
43
+ - Rakefile
44
+ - SECURITY.md
45
+ - lib/transaction_guard.rb
46
+ - lib/transaction_guard/configuration.rb
47
+ - lib/transaction_guard/detectors/http.rb
48
+ - lib/transaction_guard/detectors/job.rb
49
+ - lib/transaction_guard/detectors/mail.rb
50
+ - lib/transaction_guard/railtie.rb
51
+ - lib/transaction_guard/reporter.rb
52
+ - lib/transaction_guard/transaction.rb
53
+ - lib/transaction_guard/version.rb
54
+ - sig/transaction_guard.rbs
55
+ homepage: https://github.com/yashika279/transaction_guard
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ allowed_push_host: https://rubygems.org
60
+ homepage_uri: https://github.com/yashika279/transaction_guard
61
+ source_code_uri: https://github.com/yashika279/transaction_guard
62
+ changelog_uri: https://github.com/yashika279/transaction_guard/blob/master/CHANGELOG.md
63
+ bug_tracker_uri: https://github.com/yashika279/transaction_guard/issues
64
+ rubygems_mfa_required: 'true'
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 3.1.0
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.6.9
80
+ specification_version: 4
81
+ summary: Detect external side effects inside ActiveRecord transactions
82
+ test_files: []