Class: Hanikamu::Operation

Inherits:
Service
  • Object
show all
Includes:
ActiveModel::Validations
Defined in:
lib/hanikamu/operation.rb

Overview

:nodoc:

Defined Under Namespace

Classes: ConfigurationError, Error, FormError, GuardError, MissingBlockError

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

._blockObject (readonly)

Returns the value of attribute _block.



150
151
152
# File 'lib/hanikamu/operation.rb', line 150

def _block
  @_block
end

._mutex_expire_millisecondsObject (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_conditionObject (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_keyObject (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_conditionObject (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_klassObject (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

.configureObject

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_lockObject



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