isolator 0.0.1 → 0.1.0.pre

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
- SHA1:
3
- metadata.gz: 3989180bbf762b11ac3da685f48e49a8a52ed0f4
4
- data.tar.gz: 3b57b51b75473599d693202b08ed9077a93385a2
2
+ SHA256:
3
+ metadata.gz: 49bd3e78bcb76db6246085d139aba6aecda0ed438eb4a8241765034d92e307c0
4
+ data.tar.gz: 525b2c3d728493a7c4b57f0552dbc0434b9012db07f9b3e3b79d6b585349e5b8
5
5
  SHA512:
6
- metadata.gz: 4ce58f4e7519ee626b6c71fcff3d68c14a6bd390bc0399c65186fc9c8b71b5afe7e8218d9c7b9b927ac2cdfd20101c9c13726e3000b9af2fae1d7b68fb8ae0e6
7
- data.tar.gz: ce7c60f51a29ffd3b7a7850ad484c67cd5dec19c2d3b9ecd763514c2c985474724e9d0bce0bcdfa07ac23d748c4741d51d99bd5972c2fc4a440b9817b29c861b
6
+ metadata.gz: 8e272766ea2a5387632c203388fb0781d39897ff2a426a8fb8221b6e483bfc785214e26bff3f01eedefa1e40fa73a33816e7d617033a46d883bc57b9bdb26835
7
+ data.tar.gz: 8ca5b2671dca29a767ac8a33d8e215387850b817dda34f47a7ee455f349fc2c6e42d5fac1e07a83f8bbb2347cbf5a5d6279e4236346db0d3241e08f345253326
data/.gitignore CHANGED
@@ -10,3 +10,4 @@
10
10
  *.gem
11
11
  gemfiles/*.lock
12
12
  Gemfile.local
13
+ .rspec_status
data/.rubocop.yml CHANGED
@@ -22,6 +22,9 @@ AllCops:
22
22
  Rails:
23
23
  Enabled: false
24
24
 
25
+ Bundler/OrderedGems:
26
+ Enabled: false
27
+
25
28
  Style/SymbolArray:
26
29
  Enabled: false
27
30
 
@@ -39,6 +42,9 @@ Style/BlockDelimiters:
39
42
  Exclude:
40
43
  - 'spec/**/*.rb'
41
44
 
45
+ Style/NumericPredicate:
46
+ Enabled: false
47
+
42
48
  Layout/SpaceInsideStringInterpolation:
43
49
  EnforcedStyle: no_space
44
50
 
data/CHANGELOG.md CHANGED
@@ -2,4 +2,8 @@
2
2
 
3
3
  ## master
4
4
 
5
+ - Initial version. ([@palkan][], [@TheSmartnik][], [@alexshgov][])
6
+
5
7
  [@palkan]: https://github.com/palkan
8
+ [@alexshgov]: https://github.com/alexshgov
9
+ [@TheSmartnik]: https://github.com/TheSmartnik
data/Gemfile CHANGED
@@ -5,12 +5,14 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
5
5
  # Specify your gem's dependencies in isolator.gemspec
6
6
  gemspec
7
7
 
8
- gem 'pry-byebug'
9
- gem 'sqlite3'
10
- gem 'activerecord', '>= 5.0'
8
+ gem "pry-byebug"
11
9
 
12
- local_gemfile = 'Gemfile.local'
10
+ gem "sqlite3"
11
+
12
+ local_gemfile = File.join(__dir__, "Gemfile.local")
13
13
 
14
14
  if File.exist?(local_gemfile)
15
15
  eval(File.read(local_gemfile)) # rubocop:disable Security/Eval
16
+ else
17
+ gem "rails", "~> 5.0"
16
18
  end
data/README.md CHANGED
@@ -27,19 +27,132 @@ end
27
27
  #=> raises Isolator::BackgroundJobError
28
28
  ```
29
29
 
30
+ Of course, Isolator can detect _implicit_ transactions too. Consider this pretty common bad practice–enqueueing background job from `after_create` callback:
31
+
32
+ ```ruby
33
+ class Comment < ApplicationRecord
34
+ # the good way is to use after_create_commit
35
+ # (or not use callbacks at all)
36
+ after_create :notify_author
37
+
38
+ private
39
+
40
+ def notify_author
41
+ CommentMailer.comment_created(self).deliver_later
42
+ end
43
+ end
44
+
45
+ Comment.create(text: "Mars is watching you!")
46
+ #=> raises Isolator::BackgroundJobError
47
+ ```
48
+
49
+ Isolator is supposed to be used in tests and on staging.
50
+
30
51
  ## Installation
31
52
 
32
53
  Add this line to your application's Gemfile:
33
54
 
34
55
  ```ruby
56
+ # We suppose that Isolator is used in development and test
57
+ # environments.
35
58
  group :development, :test do
36
59
  gem "isolator"
37
60
  end
61
+
62
+ # Or you can add it to Gemfile with `require: false`
63
+ # and require it manually in your code.
64
+ #
65
+ # This approach is useful when you want to use it in staging env too.
66
+ gem "isolator", require: false
38
67
  ```
39
68
 
40
69
  ## Usage
41
70
 
42
- TBD
71
+ Isolator is a plug-n-play tool, so, it begins to work right after required.
72
+
73
+ However, there are some potential caveats:
74
+
75
+ 1) Isolator tries to detect the environment automatically and includes only necessary adapters. Thus the order of loading gems matters: make sure that `isolator` is required in the end (NOTE: in Rails, all adapters loaded after application initialization).
76
+
77
+ 2) Isolator does not distinguish framework-level adapters. For example, `:active_job` spy doesn't take into account which AJ adapter you use; if you are using a safe one (e.g. `Que`) just disable the `:active_job` adapter to avoid false negatives (i.e. `Isolator.adapters.active_job.disable!`).
78
+
79
+ 3) Isolator tries to detect the `test` environment and slightly change its behavior: first, it respect _transactional tests_; secondly, error raising is turned on by default (see [below](#configuration)).
80
+
81
+ ### Configuration
82
+
83
+ ```ruby
84
+ Isolator.configure do |config|
85
+ # Specify a custom logger to log offenses
86
+ config.logger = nil
87
+
88
+ # Raise exception on offense
89
+ config.raise_exceptions = false # true in test env
90
+
91
+ # Send notifications to uniform_notifier
92
+ config.send_notifications = false
93
+ end
94
+ ```
95
+
96
+ Isolator relys on [uniform_notifier][] to send custom notifications.
97
+
98
+ **NOTE:** `uniform_notifier` should be installed separately (i.e., added to Gemfile).
99
+
100
+ ### Supported ORMs
101
+
102
+ - `ActiveRecord` >= 4.1
103
+ - `ROM::SQL` (only if Active Support instrumentation extenstion is loaded)
104
+
105
+ ### Adapters
106
+
107
+ Isolator has a bunch of built-in adapters:
108
+ - `:http` – built on top of [Sniffer][]
109
+ - `:active_job`
110
+ - `:sidekiq`
111
+
112
+ You can dynamically enable/disable adapters, e.g.:
113
+
114
+ ```ruby
115
+ # Disable HTTP adapter == do not spy on HTTP requests
116
+ Isolator.adapters.http.disable!
117
+
118
+ # Enable back
119
+
120
+ Isolator.adapters.http.enable!
121
+ ```
122
+
123
+ ## Custom Adapters
124
+
125
+ An adapter is just a combination of a _method wrapper_ and lifecycle hooks.
126
+
127
+ Suppose that you have a class `Danger` with a method `#explode`, which is not safe to be run within a DB transaction. Then you can _isolate_ it (i.e., register with Isolator):
128
+
129
+ ```ruby
130
+ # The first argument is a unique adapter id,
131
+ # you can use it later to enable/disable the adapter
132
+ #
133
+ # The second argument is the method owner and
134
+ # the third one is a method name.
135
+ Isolotar.isolate :danger, Danger, :explode, **options
136
+
137
+ # NOTE: if you want to isolate a class method, use signleton_class instead
138
+ Isolator.isolate :danger, Danger.singleton_class, :explode, **options
139
+ ```
140
+
141
+ Possible `options` are:
142
+ - `exception_class` – an exception class to raise in case of offense
143
+ - `exception_message` – custom exception message (could be specified without a class)
144
+
145
+ You can also add some callbacks to be run before and after the transaction:
146
+
147
+ ```ruby
148
+ Isolator.before_isolate do
149
+ # right after we enter the transaction
150
+ end
151
+
152
+ Isolator.after_isolate do
153
+ # right after the transaction has been committed/rollbacked
154
+ end
155
+ ```
43
156
 
44
157
  ## Contributing
45
158
 
@@ -48,3 +161,6 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/palkan
48
161
  ## License
49
162
 
50
163
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
164
+
165
+ [Sniffer]: https://github.com/aderyabin/sniffer
166
+ [uniform_notifier]: https://github.com/flyerhzm/uniform_notifier
@@ -1,6 +1,6 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
- gem 'activerecord', '~> 4.2'
4
- gem 'sqlite3'
3
+ gem "rails", "~> 4.2"
4
+ gem "sqlite3"
5
5
 
6
- gemspec path: '..'
6
+ gemspec path: ".."
@@ -1,7 +1,7 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
- gem 'activerecord-jdbcsqlite3-adapter', '~> 50.0'
4
- gem 'jdbc-sqlite3'
5
- gem 'activerecord', '~> 5.0.0'
3
+ gem "activerecord-jdbcsqlite3-adapter", "~> 50.0"
4
+ gem "jdbc-sqlite3"
5
+ gem "rails", "~> 5.0"
6
6
 
7
- gemspec path: '..'
7
+ gemspec path: ".."
@@ -1,7 +1,7 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
- gem 'arel', github: 'rails/arel'
4
- gem 'rails', github: 'rails/rails'
5
- gem 'sqlite3'
3
+ gem "arel", github: "rails/arel"
4
+ gem "rails", github: "rails/rails"
5
+ gem "sqlite3"
6
6
 
7
- gemspec path: '..'
7
+ gemspec path: ".."
data/isolator.gemspec CHANGED
@@ -20,9 +20,16 @@ Gem::Specification.new do |spec|
20
20
  end
21
21
  spec.require_paths = ["lib"]
22
22
 
23
+ spec.add_runtime_dependency "sniffer", "~> 0.3.0"
24
+
23
25
  spec.add_development_dependency "bundler", "~> 1.14"
24
26
  spec.add_development_dependency "rake", "~> 10.0"
25
27
  spec.add_development_dependency "rspec", "~> 3.0"
28
+ spec.add_development_dependency "rspec-rails", "~> 3.0"
29
+ spec.add_development_dependency "minitest", "~> 5.10.0"
26
30
  spec.add_development_dependency "rubocop", "~> 0.51"
27
31
  spec.add_development_dependency "rubocop-md", "~> 0.2"
32
+
33
+ spec.add_development_dependency "uniform_notifier", "~> 1.11"
34
+ spec.add_development_dependency "sidekiq", "~> 5.0"
28
35
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "isolator/adapters/base"
4
+
5
+ module Isolator
6
+ # Builds adapter from provided params
7
+ module AdapterBuilder
8
+ def self.call(target, method_name, **options)
9
+ adapter = Module.new do
10
+ extend Isolator::Adapters::Base
11
+
12
+ self.exception_class = options[:exception_class] if options.key?(:exception_class)
13
+ self.exception_message = options[:exception_message] if options.key?(:exception_message)
14
+ end
15
+
16
+ add_patch_method adapter, target, method_name
17
+ adapter
18
+ end
19
+
20
+ def self.add_patch_method(adapter, base, method_name)
21
+ mod = Module.new do
22
+ define_method method_name do |*args, &block|
23
+ adapter.notify(caller) if adapter.notify_isolator?
24
+ super(*args, &block)
25
+ end
26
+ end
27
+
28
+ base.prepend mod
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ Isolator.isolate :active_job, ActiveJob::Base,
4
+ :enqueue, exception_class: Isolator::BackgroundJobError
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ Isolator.isolate :sidekiq, Sidekiq::Client,
4
+ :push, exception_class: Isolator::BackgroundJobError
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveSupport.on_load(:active_job) do
4
+ require "isolator/adapters/background_jobs/active_job"
5
+ end
6
+
7
+ require "isolator/adapters/background_jobs/sidekiq" if defined?(Sidekiq::Client)
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isolator
4
+ module Adapters
5
+ # Used as a "template" for adapters
6
+ module Base
7
+ attr_accessor :exception_class, :exception_message
8
+
9
+ def disable!
10
+ @disabled = true
11
+ end
12
+
13
+ def enable!
14
+ @disabled = false
15
+ end
16
+
17
+ def enabled?
18
+ @disabled != true
19
+ end
20
+
21
+ def notify(backtrace)
22
+ Isolator.notify(exception: build_exception, backtrace: backtrace)
23
+ end
24
+
25
+ def notify_isolator?
26
+ enabled? && Isolator.within_transaction?
27
+ end
28
+
29
+ def build_exception
30
+ klass = exception_class || Isolator::UnsafeOperationError
31
+ klass.new(exception_message)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sniffer"
4
+
5
+ Sniffer.config do |c|
6
+ # Disable Sniffer logger
7
+ c.logger = Logger.new(IO::NULL)
8
+ end
9
+
10
+ Isolator.isolate :http, Sniffer.singleton_class,
11
+ :store, exception_class: Isolator::HTTPError
12
+
13
+ Isolator.before_isolate do
14
+ Sniffer.enable!
15
+ end
16
+
17
+ Isolator.after_isolate do
18
+ Sniffer.clear!
19
+ Sniffer.disable!
20
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "isolator/adapters/http"
4
+ require "isolator/adapters/background_jobs"
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isolator
4
+ # Add before_isolate and after_isolate callbacks
5
+ module Callbacks
6
+ def before_isolate(&block)
7
+ before_isolate_callbacks << block
8
+ end
9
+
10
+ def after_isolate(&block)
11
+ after_isolate_callbacks << block
12
+ end
13
+
14
+ def start!
15
+ before_isolate_callbacks.each(&:call)
16
+ end
17
+
18
+ def finish!
19
+ after_isolate_callbacks.each(&:call)
20
+ end
21
+
22
+ def before_isolate_callbacks
23
+ @before_isolate_callbacks ||= []
24
+ end
25
+
26
+ def after_isolate_callbacks
27
+ @after_isolate_callbacks ||= []
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isolator
4
+ # Isolator configuration:
5
+ #
6
+ # - `raise_exceptions` - whether to raise an exception in case of offense;
7
+ # defaults to true in test env and false otherwise.
8
+ # NOTE: env is infered from RACK_ENV and RAILS_ENV.
9
+ #
10
+ # - `logger` - logger instance (nil by default)
11
+ #
12
+ # - `send_notifications` - whether to send notifications (through uniform_notifier);
13
+ # defauls to false
14
+ class Configuration
15
+ attr_accessor :raise_exceptions, :logger, :send_notifications
16
+
17
+ def initialize
18
+ @logger = nil
19
+ @raise_exceptions = test_env?
20
+ @send_notifications = false
21
+ end
22
+
23
+ alias raise_exceptions? raise_exceptions
24
+ alias send_notifications? send_notifications
25
+
26
+ def test_env?
27
+ ENV["RACK_ENV"] == "test" || ENV["RAILS_ENV"] == "test"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isolator # :nodoc: all
4
+ class UnsafeOperationError < StandardError
5
+ MESSAGE = "You are trying to do unsafe operation inside db transaction"
6
+
7
+ def initialize(msg = nil)
8
+ msg ||= self.class::MESSAGE
9
+ super
10
+ end
11
+ end
12
+
13
+ class HTTPError < UnsafeOperationError
14
+ MESSAGE = "You are trying to make an outgoing network request inside db transaction. "
15
+ end
16
+
17
+ class BackgroundJobError < UnsafeOperationError
18
+ MESSAGE = "You are trying to enqueue background job inside db transaction. " \
19
+ "In case of transaction failure, this may lead to data inconsistency and unexpected bugs"
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isolator
4
+ module ThreadFetch # :nodoc:
5
+ refine Thread do
6
+ def fetch(key, fallback = :__undef__)
7
+ raise KeyError, "key not found: #{key}" if !key?(key) && fallback == :__undef__
8
+
9
+ self[key] || fallback
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isolator
4
+ # Add .isolate function to build and register adapters
5
+ module Isolate
6
+ def isolate(id, target_module, method_name, **options)
7
+ raise "Adapter already registered: #{id}" if Isolator.adapters.key?(id.to_s)
8
+ adapter = AdapterBuilder.call(target_module, method_name, **options)
9
+ Isolator.adapters[id.to_s] = adapter
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isolator
4
+ # Wrapper over different notifications methods (exceptions, logging, uniform notifier)
5
+ class Notifier
6
+ attr_reader :exception, :backtrace
7
+
8
+ def initialize(exception, backtrace = caller)
9
+ @exception = exception
10
+ @backtrace = backtrace
11
+ end
12
+
13
+ def call
14
+ log_exception
15
+ send_notifications if send_notifications?
16
+ raise(exception.class, exception.message, filtered_backtrace) if raise_exceptions?
17
+ end
18
+
19
+ private
20
+
21
+ def raise_exceptions?
22
+ Isolator.config.raise_exceptions?
23
+ end
24
+
25
+ def send_notifications?
26
+ Isolator.config.send_notifications?
27
+ end
28
+
29
+ def log_exception
30
+ return unless Isolator.config.logger
31
+ Isolator.config.logger.warn(
32
+ "[ISOLATOR EXCEPTION]\n" \
33
+ "#{exception.message}\n" \
34
+ " ↳ #{filtered_backtrace.first}"
35
+ )
36
+ end
37
+
38
+ def send_notifications
39
+ return unless uniform_notifier_loaded?
40
+
41
+ ::UniformNotifier.active_notifiers.each do |notifier|
42
+ notifier.out_of_channel_notify exception.message
43
+ end
44
+ end
45
+
46
+ def filtered_backtrace
47
+ backtrace.reject { |line| line =~ /gems/ }.take_while { |line| line !~ /ruby/ }
48
+ end
49
+
50
+ def uniform_notifier_loaded?
51
+ return true if defined?(::UniformNotifier)
52
+
53
+ begin
54
+ require "uniform_notifier"
55
+ rescue LoadError
56
+ warn(
57
+ "Please, install and configure 'uniform_notifier' to send notifications:\n" \
58
+ "# Gemfile\n" \
59
+ "gem 'uniform_notifer', '~> 1.11', require: false"
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./active_support_subscriber"
4
+
5
+ Isolator::ActiveSupportSubscriber.subscribe!("sql.active_record")
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isolator
4
+ # ActiveSupport notifications listener
5
+ # Used for ActiveRecord and ROM::SQL (when instrumentation is available)
6
+ module ActiveSupportSubscriber
7
+ START_PATTERN = %r{(\ABEGIN|\ASAVEPOINT)}xi
8
+ FINISH_PATTERN = %r{(\ACOMMIT|\AROLLBACK|\ARELEASE|\AEND TRANSACTION)}xi
9
+
10
+ def self.subscribe!(event)
11
+ ::ActiveSupport::Notifications.subscribe(event) do |_name, _start, _finish, _id, query|
12
+ Isolator.incr_transactions! if query[:sql] =~ START_PATTERN
13
+ Isolator.decr_transactions! if query[:sql] =~ FINISH_PATTERN
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./active_support_subscriber"
4
+
5
+ Isolator::ActiveSupportSubscriber.subscribe!("sql.rom")
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(ActiveSupport)
4
+ ActiveSupport.on_load(:active_record) do
5
+ require "isolator/orm_adapters/active_record"
6
+ end
7
+
8
+ require "isolator/orm_adapter/rom_active_support" if
9
+ defined?(::ROM::SQL::ActiveSupportInstrumentation)
10
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isolator
4
+ class Railtie < ::Rails::Railtie # :nodoc:
5
+ config.after_initialize do
6
+ # Load adapters
7
+ require "isolator/adapters"
8
+
9
+ next unless Rails.env.test?
10
+
11
+ if defined?(::ActiveRecord::TestFixtures)
12
+ ::ActiveRecord::TestFixtures.prepend(
13
+ Module.new do
14
+ def setup_fixtures(*)
15
+ super
16
+ return unless run_in_transaction?
17
+
18
+ open_count = ActiveRecord::Base.connection.open_transactions
19
+ Isolator.transactions_threshold += open_count
20
+ end
21
+
22
+ def teardown_fixtures(*)
23
+ if run_in_transaction?
24
+ open_count = ActiveRecord::Base.connection.open_transactions
25
+ Isolator.transactions_threshold -= open_count
26
+ end
27
+ super
28
+ end
29
+ end
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isolator
4
+ # Hash with key accessors
5
+ class SimpleHashie < Hash
6
+ def method_missing(key, *args, &block)
7
+ key_str = key.to_s
8
+
9
+ if key_str.end_with?("=")
10
+ self[key_str.tr("=")] = args.first
11
+ else
12
+ fetch(key_str) { super }
13
+ end
14
+ end
15
+
16
+ def respond_to_missing?(key)
17
+ key_str = key.to_s
18
+ if key_str.end_with?("=")
19
+ key?(key_str.tr("=")) || super
20
+ else
21
+ key?(key_str) || super
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Isolator
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0.pre"
5
5
  end
data/lib/isolator.rb CHANGED
@@ -1,6 +1,93 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "isolator/version"
4
+ require "isolator/configuration"
5
+ require "isolator/adapter_builder"
6
+ require "isolator/notifier"
7
+ require "isolator/errors"
8
+ require "isolator/simple_hashie"
4
9
 
5
- module Isolator # :nodoc:
10
+ require "isolator/callbacks"
11
+ require "isolator/isolate"
12
+
13
+ require "isolator/ext/thread_fetch"
14
+
15
+ # Isolator detects unsafe operations performed within DB transactions.
16
+ module Isolator
17
+ using Isolator::ThreadFetch
18
+
19
+ class << self
20
+ def config
21
+ @config ||= Configuration.new
22
+ end
23
+
24
+ def configure
25
+ yield config
26
+ end
27
+
28
+ def notify(exception:, backtrace:)
29
+ Notifier.new(exception, backtrace).call
30
+ end
31
+
32
+ def enable!
33
+ Thread.current[:isolator_disabled] = false
34
+ end
35
+
36
+ def disable!
37
+ Thread.current[:isolator_disabled] = true
38
+ end
39
+
40
+ def transactions_threshold
41
+ Thread.current.fetch(:isolator_threshold)
42
+ end
43
+
44
+ def transactions_threshold=(val)
45
+ Thread.current[:isolator_threshold] = val
46
+ end
47
+
48
+ def incr_transactions!
49
+ return unless enabled?
50
+ Thread.current[:isolator_transactions] =
51
+ Thread.current.fetch(:isolator_transactions, 0) + 1
52
+ start! if Thread.current.fetch(:isolator_transactions) == transactions_threshold
53
+ end
54
+
55
+ def decr_transactions!
56
+ return unless enabled?
57
+ Thread.current[:isolator_transactions] =
58
+ Thread.current.fetch(:isolator_transactions) - 1
59
+ finish! if Thread.current.fetch(:isolator_transactions) == (transactions_threshold - 1)
60
+ end
61
+
62
+ def clear_transactions!
63
+ Thread.current[:isolator_transactions] = 0
64
+ end
65
+
66
+ def within_transaction?
67
+ Thread.current.fetch(:isolator_transactions, 0) >= transactions_threshold
68
+ end
69
+
70
+ def enabled?
71
+ Thread.current[:isolator_disabled] != true
72
+ end
73
+
74
+ def adapters
75
+ @adapters ||= Isolator::SimpleHashie.new
76
+ end
77
+
78
+ include Isolator::Isolate
79
+ include Isolator::Callbacks
80
+ end
81
+
82
+ self.transactions_threshold = 1
83
+ end
84
+
85
+ require "isolator/orm_adapters"
86
+
87
+ # Load adapters after application initialization
88
+ # (when all deps are likely loaded).
89
+ if defined?(Rails)
90
+ require "isolator/railtie"
91
+ else
92
+ require "isolator/adapters"
6
93
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: isolator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-01-28 00:00:00.000000000 Z
11
+ date: 2018-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sniffer
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.3.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.3.0
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +66,34 @@ dependencies:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
68
  version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 5.10.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 5.10.0
55
97
  - !ruby/object:Gem::Dependency
56
98
  name: rubocop
57
99
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +122,34 @@ dependencies:
80
122
  - - "~>"
81
123
  - !ruby/object:Gem::Version
82
124
  version: '0.2'
125
+ - !ruby/object:Gem::Dependency
126
+ name: uniform_notifier
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.11'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.11'
139
+ - !ruby/object:Gem::Dependency
140
+ name: sidekiq
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '5.0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '5.0'
83
153
  description: Detect non-atomic interactions within DB transactions
84
154
  email:
85
155
  - dementiev.vm@gmail.com
@@ -102,6 +172,25 @@ files:
102
172
  - gemfiles/railsmaster.gemfile
103
173
  - isolator.gemspec
104
174
  - lib/isolator.rb
175
+ - lib/isolator/adapter_builder.rb
176
+ - lib/isolator/adapters.rb
177
+ - lib/isolator/adapters/background_jobs.rb
178
+ - lib/isolator/adapters/background_jobs/active_job.rb
179
+ - lib/isolator/adapters/background_jobs/sidekiq.rb
180
+ - lib/isolator/adapters/base.rb
181
+ - lib/isolator/adapters/http.rb
182
+ - lib/isolator/callbacks.rb
183
+ - lib/isolator/configuration.rb
184
+ - lib/isolator/errors.rb
185
+ - lib/isolator/ext/thread_fetch.rb
186
+ - lib/isolator/isolate.rb
187
+ - lib/isolator/notifier.rb
188
+ - lib/isolator/orm_adapters.rb
189
+ - lib/isolator/orm_adapters/active_record.rb
190
+ - lib/isolator/orm_adapters/active_support_subscriber.rb
191
+ - lib/isolator/orm_adapters/rom_active_support.rb
192
+ - lib/isolator/railtie.rb
193
+ - lib/isolator/simple_hashie.rb
105
194
  - lib/isolator/version.rb
106
195
  homepage: https://github.com/palkan/isolator
107
196
  licenses:
@@ -118,12 +207,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
118
207
  version: 2.3.0
119
208
  required_rubygems_version: !ruby/object:Gem::Requirement
120
209
  requirements:
121
- - - ">="
210
+ - - ">"
122
211
  - !ruby/object:Gem::Version
123
- version: '0'
212
+ version: 1.3.1
124
213
  requirements: []
125
214
  rubyforge_project:
126
- rubygems_version: 2.6.13
215
+ rubygems_version: 2.7.4
127
216
  signing_key:
128
217
  specification_version: 4
129
218
  summary: Detect non-atomic interactions within DB transactions