isolator 0.0.1 → 0.1.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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