Class: Hanikamu::Operation
- Inherits:
-
Service
- Object
- Service
- Hanikamu::Operation
- Includes:
- ActiveModel::Validations
- Defined in:
- lib/hanikamu/operation.rb
Overview
:nodoc:
Defined Under Namespace
Classes: ConfigurationError, Error, FormError, GuardError, MissingBlockError
Class Attribute Summary collapse
-
._block ⇒ Object
readonly
Returns the value of attribute _block.
-
._mutex_expire_milliseconds ⇒ Object
readonly
Returns the value of attribute _mutex_expire_milliseconds.
-
._mutex_if_condition ⇒ Object
readonly
Returns the value of attribute _mutex_if_condition.
-
._mutex_lock_key ⇒ Object
readonly
Returns the value of attribute _mutex_lock_key.
-
._mutex_unless_condition ⇒ Object
readonly
Returns the value of attribute _mutex_unless_condition.
-
._transaction_klass ⇒ Object
readonly
Returns the value of attribute _transaction_klass.
Class Method Summary collapse
- .block(bool) ⇒ Object
-
.configure ⇒ Object
Override configure to cascade whitelisted_errors to Hanikamu::Service.
-
.guard(&block) ⇒ Object
Define guard validations using a block The block is evaluated in the context of a Guard class.
- .redis_lock ⇒ Object
-
.within_mutex(lock_key, expire_milliseconds: nil, if: nil, unless: nil) ⇒ Object
DSL methods.
- .within_transaction(klass) ⇒ Object
Instance Method Summary collapse
- #call!(&block) ⇒ Object
- #validate_block!(&block) ⇒ Object
- #within_mutex! ⇒ Object
- #within_transaction! ⇒ Object
Class Attribute Details
._block ⇒ Object (readonly)
Returns the value of attribute _block.
150 151 152 |
# File 'lib/hanikamu/operation.rb', line 150 def _block @_block end |
._mutex_expire_milliseconds ⇒ Object (readonly)
Returns the value of attribute _mutex_expire_milliseconds.
150 151 152 |
# File 'lib/hanikamu/operation.rb', line 150 def _mutex_expire_milliseconds @_mutex_expire_milliseconds end |
._mutex_if_condition ⇒ Object (readonly)
Returns the value of attribute _mutex_if_condition.
150 151 152 |
# File 'lib/hanikamu/operation.rb', line 150 def _mutex_if_condition @_mutex_if_condition end |
._mutex_lock_key ⇒ Object (readonly)
Returns the value of attribute _mutex_lock_key.
150 151 152 |
# File 'lib/hanikamu/operation.rb', line 150 def _mutex_lock_key @_mutex_lock_key end |
._mutex_unless_condition ⇒ Object (readonly)
Returns the value of attribute _mutex_unless_condition.
150 151 152 |
# File 'lib/hanikamu/operation.rb', line 150 def _mutex_unless_condition @_mutex_unless_condition end |
._transaction_klass ⇒ Object (readonly)
Returns the value of attribute _transaction_klass.
150 151 152 |
# File 'lib/hanikamu/operation.rb', line 150 def _transaction_klass @_transaction_klass end |
Class Method Details
.block(bool) ⇒ Object
108 109 110 |
# File 'lib/hanikamu/operation.rb', line 108 def block(bool) @_block = bool end |
.configure ⇒ Object
Override configure to cascade whitelisted_errors to Hanikamu::Service
56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/hanikamu/operation.rb', line 56 def self.configure super do |config| yield(config) if block_given? # Always include Redlock::LockError alongside user-provided errors whitelisted_errors = ([Redlock::LockError] + Array(config.whitelisted_errors)).uniq # Set on both Operation and Service configs because: # - Operation.config is checked when .call is invoked on Operation subclasses # - Service.config is set for consistency when directly calling Hanikamu::Service config.whitelisted_errors = whitelisted_errors Hanikamu::Service.config.whitelisted_errors = whitelisted_errors end end |
.guard(&block) ⇒ Object
Define guard validations using a block The block is evaluated in the context of a Guard class
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/hanikamu/operation.rb', line 114 def guard(&block) # rubocop:disable Metrics/MethodLength return unless block # Thread-safe constant definition with mutex @guard_definition_mutex ||= Mutex.new @guard_definition_mutex.synchronize do # Remove existing Guard constant if it exists to support Rails reloading remove_const(:Guard) if const_defined?(:Guard, false) # Create a new Guard class with ActiveModel validations guard_class = Class.new do include ActiveModel::Validations attr_reader :operation alias service operation def initialize(operation) @operation = operation end # Helper to delegate methods to operation/service def self.delegates(*methods) methods.each do |method_name| define_method(method_name) do operation.public_send(method_name) end end end class_eval(&block) end const_set(:Guard, guard_class) end end |
.redis_lock ⇒ Object
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/hanikamu/operation.rb', line 72 def redis_lock @redis_lock ||= begin unless config.redis_client raise( ConfigurationError, "Hanikamu::Operation.config.redis_client is not configured. " \ "Please set it in an initializer: Hanikamu::Operation.config.redis_client = your_redis_client" ) end Redlock::Client.new( [config.redis_client], retry_count: config.redlock_retry_count, retry_delay: config.redlock_retry_delay, retry_jitter: config.redlock_retry_jitter, redis_timeout: config.redlock_timeout ) end end |
.within_mutex(lock_key, expire_milliseconds: nil, if: nil, unless: nil) ⇒ Object
DSL methods
93 94 95 96 97 98 99 100 101 102 |
# File 'lib/hanikamu/operation.rb', line 93 def within_mutex(lock_key, expire_milliseconds: nil, if: nil, unless: nil) if_condition = binding.local_variable_get(:if) unless_condition = binding.local_variable_get(:unless) validate_mutex_conditions!(if_condition, unless_condition) @_mutex_lock_key = lock_key @_mutex_expire_milliseconds = expire_milliseconds || Hanikamu::Operation.config.mutex_expire_milliseconds @_mutex_if_condition = if_condition @_mutex_unless_condition = unless_condition end |
.within_transaction(klass) ⇒ Object
104 105 106 |
# File 'lib/hanikamu/operation.rb', line 104 def within_transaction(klass) @_transaction_klass = klass end |
Instance Method Details
#call!(&block) ⇒ Object
170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/hanikamu/operation.rb', line 170 def call!(&block) validate_block!(&block) within_mutex! do validate! guard! within_transaction! do block ? execute(&block) : execute end end end |
#validate_block!(&block) ⇒ Object
183 184 185 186 187 |
# File 'lib/hanikamu/operation.rb', line 183 def validate_block!(&block) return unless self.class._block raise Hanikamu::Operation::MissingBlockError, "This service requires a block to be called" unless block end |
#within_mutex! ⇒ Object
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
# File 'lib/hanikamu/operation.rb', line 189 def within_mutex!(&) return yield if self.class._mutex_lock_key.blank? return yield unless _should_apply_mutex? # Snapshot the resolved key: it is documented as a String, and freezing a # copy prevents operation code from mutating the same object mid-run and # desyncing the reentrancy registry / lease bookkeeping (the cleanup would # otherwise look up a different value and leak the entry). lock_key = _stable_lock_key(public_send(self.class._mutex_lock_key)) # Reentrancy: a nested operation invoked synchronously (e.g. from a synchronous # event handler, or a nested service call) while this context still holds this # exact key would otherwise re-acquire it. Redlock is not reentrant, so the same # execution context would block on itself until the TTL expires and then raise # Redlock::LockError. Skip the re-acquire and run inline; only real acquisitions # talk to Redis. Different fibers/threads/processes still contend normally. # # The bypass is gated on a *live* lease, not merely lexical nesting: we bypass # only while the innermost lease this context holds on the key is still valid. # Once it could have lapsed we fall through to a real acquire — which re-acquires # the key if it is free, or raises Redlock::LockError if it was taken over. return yield if _reentrant_lease_valid?(lock_key) _acquire_and_run(lock_key, &) end |
#within_transaction! ⇒ Object
215 216 217 218 219 |
# File 'lib/hanikamu/operation.rb', line 215 def within_transaction!(&) return yield if transaction_class.nil? transaction_class.transaction(&) end |