hanikamu-operation 0.1.2 → 0.2.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 +4 -4
- data/.cursor/rules/docker-environment.mdc +30 -0
- data/.vscode/settings.json +5 -0
- data/Dockerfile +1 -1
- data/Makefile +4 -0
- data/README.md +34 -2
- data/lib/hanikamu/operation.rb +37 -11
- metadata +5 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b935969d9ffb9b34935f54e2f9d8adf907603906916a4c1b555499bd25e97f28
|
|
4
|
+
data.tar.gz: 2fada67131e16535eb5c6480c3a80999315efd0306753aae615a6a582d9cb43d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc5970b13fd6511a024b14071b273350604ef811f8bb944ca03f6839ed7f03051d106c63f550225f2b4de181d60c7698f545ecd1219faa1bb813aaa76be340a3
|
|
7
|
+
data.tar.gz: a691099d6fb2be177d4e19bcfff74e9760716465b2a9ceaff2fa0df33a63027e199377749e285f4a8b9cad02224c93e347e4b229eab60f159aa2d4bc5e585454
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Development environment runs via docker-compose. Use Makefile targets instead of local commands.
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Docker-Compose Development Environment
|
|
7
|
+
|
|
8
|
+
This project runs entirely inside Docker. Never run Ruby, Bundler, or Redis commands directly on the host.
|
|
9
|
+
|
|
10
|
+
## Makefile Targets
|
|
11
|
+
|
|
12
|
+
Use these instead of raw commands:
|
|
13
|
+
|
|
14
|
+
| Task | Command |
|
|
15
|
+
|------|---------|
|
|
16
|
+
| Run tests | `make rspec` |
|
|
17
|
+
| Run linter (auto-fix) | `make cops` |
|
|
18
|
+
| Open a shell in the container | `make shell` |
|
|
19
|
+
| Open an IRB console | `make console` |
|
|
20
|
+
| Install gems & rebuild | `make bundle` |
|
|
21
|
+
| Build the Docker image | `make build` |
|
|
22
|
+
|
|
23
|
+
## Important
|
|
24
|
+
|
|
25
|
+
- All `make` targets use `docker-compose run --rm app` under the hood.
|
|
26
|
+
- Redis is provided as a linked service (`redis://redis:6379/15`) — no local Redis needed.
|
|
27
|
+
- After changing the Gemfile or gemspec, run `make build` to reinstall dependencies.
|
|
28
|
+
- Never use `bundle exec` directly on the host; always go through `make` or `make shell`.
|
|
29
|
+
- Run `make` commands directly without `cd` — use the `working_directory` parameter instead.
|
|
30
|
+
- After every code change, run `make rspec` to verify specs still pass before moving on.
|
data/Dockerfile
CHANGED
data/Makefile
CHANGED
|
@@ -14,6 +14,10 @@ console: ## build the image
|
|
|
14
14
|
rspec: ## build the image
|
|
15
15
|
docker-compose run --rm app bash -c "bundle exec rspec"
|
|
16
16
|
|
|
17
|
+
.PHONY: bundle
|
|
18
|
+
bundle: ## install gems and rebuild image
|
|
19
|
+
docker-compose run --rm app bundle install
|
|
20
|
+
$(MAKE) build
|
|
17
21
|
|
|
18
22
|
.PHONY: cops
|
|
19
23
|
cops: ## build the image
|
data/README.md
CHANGED
|
@@ -66,7 +66,7 @@ Requires Ruby 3.4.0 or later.
|
|
|
66
66
|
|
|
67
67
|
```ruby
|
|
68
68
|
# Gemfile
|
|
69
|
-
gem 'hanikamu-operation', '~> 0.
|
|
69
|
+
gem 'hanikamu-operation', '~> 0.2.0'
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
```bash
|
|
@@ -135,7 +135,7 @@ Requires Ruby 3.4.0 or later.
|
|
|
135
135
|
|
|
136
136
|
```ruby
|
|
137
137
|
# Gemfile
|
|
138
|
-
gem 'hanikamu-operation', '~> 0.
|
|
138
|
+
gem 'hanikamu-operation', '~> 0.2.0'
|
|
139
139
|
```
|
|
140
140
|
|
|
141
141
|
```bash
|
|
@@ -332,6 +332,38 @@ def mutex_lock
|
|
|
332
332
|
end
|
|
333
333
|
```
|
|
334
334
|
|
|
335
|
+
**Conditional locking with `:if` / `:unless`**:
|
|
336
|
+
|
|
337
|
+
When the lock key is derived from an optional attribute, a `nil` value produces a degenerate shared key (e.g., `"Order$"`) that serializes all operations. Use `:if` or `:unless` to skip the Redis lock entirely when it isn't needed:
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
class ProcessOrderOperation < Hanikamu::Operation
|
|
341
|
+
attribute :order_id?, Types::Params::Integer.optional
|
|
342
|
+
|
|
343
|
+
within_mutex(:mutex_lock, if: -> { order_id.present? })
|
|
344
|
+
|
|
345
|
+
def execute
|
|
346
|
+
# ...
|
|
347
|
+
response(successful: true)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def mutex_lock
|
|
351
|
+
"Order$#{order_id}"
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
- When `order_id` is present, the lock is acquired on `"Order$42"` as usual.
|
|
357
|
+
- When `order_id` is `nil`, no Redis round-trip occurs — the operation runs without a lock.
|
|
358
|
+
|
|
359
|
+
The `:unless` option works inversely:
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
within_mutex(:mutex_lock, unless: -> { order_id.nil? })
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
The lambda is evaluated via `instance_exec` on the operation instance, so it has access to all attributes and methods. Providing both `:if` and `:unless` raises `ArgumentError`.
|
|
366
|
+
|
|
335
367
|
### Database Transactions with `within_transaction`
|
|
336
368
|
|
|
337
369
|
Ensure multiple database changes succeed or fail together atomically. If any database operation raises an exception, all changes are rolled back.
|
data/lib/hanikamu/operation.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Hanikamu
|
|
|
6
6
|
class Operation < Hanikamu::Service
|
|
7
7
|
include ActiveModel::Validations
|
|
8
8
|
|
|
9
|
-
Error
|
|
9
|
+
class Error < Hanikamu::Service::Error; end
|
|
10
10
|
|
|
11
11
|
# Error classes
|
|
12
12
|
class FormError < Hanikamu::Service::Error
|
|
@@ -91,9 +91,15 @@ module Hanikamu
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
# DSL methods
|
|
94
|
-
def within_mutex(lock_key, expire_milliseconds: nil)
|
|
94
|
+
def within_mutex(lock_key, expire_milliseconds: nil, if: nil, unless: nil)
|
|
95
|
+
if_condition = binding.local_variable_get(:if)
|
|
96
|
+
unless_condition = binding.local_variable_get(:unless)
|
|
97
|
+
validate_mutex_conditions!(if_condition, unless_condition)
|
|
98
|
+
|
|
95
99
|
@_mutex_lock_key = lock_key
|
|
96
100
|
@_mutex_expire_milliseconds = expire_milliseconds || Hanikamu::Operation.config.mutex_expire_milliseconds
|
|
101
|
+
@_mutex_if_condition = if_condition
|
|
102
|
+
@_mutex_unless_condition = unless_condition
|
|
97
103
|
end
|
|
98
104
|
|
|
99
105
|
def within_transaction(klass)
|
|
@@ -144,7 +150,24 @@ module Hanikamu
|
|
|
144
150
|
end
|
|
145
151
|
# rubocop:enable Metrics/MethodLength
|
|
146
152
|
|
|
147
|
-
attr_reader :_mutex_lock_key, :_mutex_expire_milliseconds, :
|
|
153
|
+
attr_reader :_mutex_lock_key, :_mutex_expire_milliseconds, :_mutex_if_condition, :_mutex_unless_condition,
|
|
154
|
+
:_transaction_klass, :_block
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def validate_mutex_conditions!(if_condition, unless_condition)
|
|
159
|
+
if if_condition && unless_condition
|
|
160
|
+
raise ArgumentError, "Cannot specify both :if and :unless conditions for within_mutex"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if if_condition && !if_condition.respond_to?(:call)
|
|
164
|
+
raise ArgumentError, "within_mutex :if condition must be a callable (e.g. a Proc or lambda)"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
return unless unless_condition && !unless_condition.respond_to?(:call)
|
|
168
|
+
|
|
169
|
+
raise ArgumentError, "within_mutex :unless condition must be a callable (e.g. a Proc or lambda)"
|
|
170
|
+
end
|
|
148
171
|
end
|
|
149
172
|
|
|
150
173
|
def call!(&block)
|
|
@@ -167,15 +190,11 @@ module Hanikamu
|
|
|
167
190
|
end
|
|
168
191
|
|
|
169
192
|
def within_mutex!(&)
|
|
170
|
-
return yield if
|
|
171
|
-
|
|
172
|
-
Hanikamu::Operation.redis_lock.lock!(_lock_key, self.class._mutex_expire_milliseconds, &)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def _lock_key
|
|
176
|
-
return if self.class._mutex_lock_key.blank?
|
|
193
|
+
return yield if self.class._mutex_lock_key.blank?
|
|
194
|
+
return yield unless _should_apply_mutex?
|
|
177
195
|
|
|
178
|
-
public_send(self.class._mutex_lock_key)
|
|
196
|
+
lock_key = public_send(self.class._mutex_lock_key)
|
|
197
|
+
Hanikamu::Operation.redis_lock.lock!(lock_key, self.class._mutex_expire_milliseconds, &)
|
|
179
198
|
end
|
|
180
199
|
|
|
181
200
|
def within_transaction!(&)
|
|
@@ -186,6 +205,13 @@ module Hanikamu
|
|
|
186
205
|
|
|
187
206
|
private
|
|
188
207
|
|
|
208
|
+
def _should_apply_mutex?
|
|
209
|
+
return false if self.class._mutex_if_condition && !instance_exec(&self.class._mutex_if_condition)
|
|
210
|
+
return false if self.class._mutex_unless_condition && instance_exec(&self.class._mutex_unless_condition)
|
|
211
|
+
|
|
212
|
+
true
|
|
213
|
+
end
|
|
214
|
+
|
|
189
215
|
def transaction_class
|
|
190
216
|
return if self.class._transaction_klass.nil?
|
|
191
217
|
return ActiveRecord::Base if self.class._transaction_klass == :base
|
metadata
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hanikamu-operation
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nicolai Seerup
|
|
8
8
|
- Alejandro Jimenez
|
|
9
|
-
autorequire:
|
|
10
9
|
bindir: exe
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
13
12
|
dependencies:
|
|
14
13
|
- !ruby/object:Gem::Dependency
|
|
15
14
|
name: activemodel
|
|
@@ -102,11 +101,12 @@ dependencies:
|
|
|
102
101
|
description: |
|
|
103
102
|
Ruby gem for building robust service operations with guard validations, distributed mutex locks via Redlock,
|
|
104
103
|
database transactions, and comprehensive error handling. Thread-safe and designed for production Rails applications.
|
|
105
|
-
email:
|
|
106
104
|
executables: []
|
|
107
105
|
extensions: []
|
|
108
106
|
extra_rdoc_files: []
|
|
109
107
|
files:
|
|
108
|
+
- ".cursor/rules/docker-environment.mdc"
|
|
109
|
+
- ".vscode/settings.json"
|
|
110
110
|
- CHANGELOG.md
|
|
111
111
|
- Dockerfile
|
|
112
112
|
- LICENSE.txt
|
|
@@ -124,7 +124,6 @@ metadata:
|
|
|
124
124
|
source_code_uri: https://github.com/Hanikamu/hanikamu-operation
|
|
125
125
|
changelog_uri: https://github.com/Hanikamu/hanikamu-operation/blob/main/CHANGELOG.md
|
|
126
126
|
rubygems_mfa_required: 'true'
|
|
127
|
-
post_install_message:
|
|
128
127
|
rdoc_options: []
|
|
129
128
|
require_paths:
|
|
130
129
|
- lib
|
|
@@ -139,8 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
139
138
|
- !ruby/object:Gem::Version
|
|
140
139
|
version: '0'
|
|
141
140
|
requirements: []
|
|
142
|
-
rubygems_version:
|
|
143
|
-
signing_key:
|
|
141
|
+
rubygems_version: 4.0.3
|
|
144
142
|
specification_version: 4
|
|
145
143
|
summary: Service objects with guards, distributed locks, and transactions
|
|
146
144
|
test_files: []
|