hanikamu-operation 0.1.1 → 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/CHANGELOG.md +7 -0
- data/Dockerfile +1 -1
- data/Makefile +4 -0
- data/README.md +98 -183
- data/lib/hanikamu/operation.rb +37 -11
- metadata +6 -8
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/CHANGELOG.md
CHANGED
|
@@ -7,3 +7,10 @@
|
|
|
7
7
|
## [0.1.1] - 2025-11-26
|
|
8
8
|
|
|
9
9
|
- Updated Gemfile.lock
|
|
10
|
+
|
|
11
|
+
## [0.1.2] - 2025-12-05
|
|
12
|
+
|
|
13
|
+
- **Breaking Change**: Minimum Ruby version is now 3.4.0
|
|
14
|
+
- Removed `redis-client` as direct dependency (now transitive through `redlock`)
|
|
15
|
+
- Updated CI to test only Ruby 3.4
|
|
16
|
+
- Improved README with clearer FormError vs GuardError examples
|
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
|
@@ -8,9 +8,8 @@ A Ruby gem that extends [hanikamu-service](https://github.com/Hanikamu/hanikamu-
|
|
|
8
8
|
|
|
9
9
|
1. [Why Hanikamu::Operation?](#why-hanikamuoperation)
|
|
10
10
|
2. [Quick Start](#quick-start)
|
|
11
|
-
3. [
|
|
12
|
-
4. [
|
|
13
|
-
5. [Usage](#usage)
|
|
11
|
+
3. [Setup](#setup)
|
|
12
|
+
4. [Usage](#usage)
|
|
14
13
|
- [Basic Operation](#basic-operation)
|
|
15
14
|
- [Distributed Locking](#distributed-locking-with-within_mutex)
|
|
16
15
|
- [Database Transactions](#database-transactions-with-within_transaction)
|
|
@@ -18,14 +17,14 @@ A Ruby gem that extends [hanikamu-service](https://github.com/Hanikamu/hanikamu-
|
|
|
18
17
|
- [Guard Conditions](#guard-conditions)
|
|
19
18
|
- [Block Requirements](#block-requirements)
|
|
20
19
|
- [Complete Example](#complete-example-combining-all-features)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
5. [Error Handling](#error-handling)
|
|
21
|
+
6. [Best Practices](#best-practices)
|
|
22
|
+
7. [Configuration Reference](#configuration-reference)
|
|
23
|
+
8. [Testing](#testing)
|
|
24
|
+
9. [Development](#development)
|
|
25
|
+
10. [Contributing](#contributing)
|
|
26
|
+
11. [License](#license)
|
|
27
|
+
12. [Credits](#credits)
|
|
29
28
|
|
|
30
29
|
## Why Hanikamu::Operation?
|
|
31
30
|
|
|
@@ -63,9 +62,11 @@ Use `Hanikamu::Operation` (instead of plain `Hanikamu::Service`) when your busin
|
|
|
63
62
|
|
|
64
63
|
**1. Install the gem**
|
|
65
64
|
|
|
65
|
+
Requires Ruby 3.4.0 or later.
|
|
66
|
+
|
|
66
67
|
```ruby
|
|
67
68
|
# Gemfile
|
|
68
|
-
gem 'hanikamu-operation', '~> 0.
|
|
69
|
+
gem 'hanikamu-operation', '~> 0.2.0'
|
|
69
70
|
```
|
|
70
71
|
|
|
71
72
|
```bash
|
|
@@ -122,32 +123,19 @@ else
|
|
|
122
123
|
end
|
|
123
124
|
```
|
|
124
125
|
|
|
125
|
-
## Installation
|
|
126
|
-
|
|
127
|
-
Add to your application's Gemfile:
|
|
128
|
-
|
|
129
|
-
```ruby
|
|
130
|
-
gem 'hanikamu-operation', '~> 0.1.1'
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Then execute:
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
bundle install
|
|
137
|
-
```
|
|
138
|
-
|
|
139
126
|
## Setup
|
|
140
127
|
|
|
141
|
-
### Rails Application Setup
|
|
128
|
+
### Rails Application Setup
|
|
142
129
|
|
|
143
130
|
Follow these steps to integrate Hanikamu::Operation into a Rails application:
|
|
144
131
|
|
|
145
132
|
**Step 1: Add the gem to your Gemfile**
|
|
146
133
|
|
|
134
|
+
Requires Ruby 3.4.0 or later.
|
|
135
|
+
|
|
147
136
|
```ruby
|
|
148
137
|
# Gemfile
|
|
149
|
-
gem 'hanikamu-operation', '~> 0.
|
|
150
|
-
gem 'redis-client', '~> 0.22' # Required for distributed locking
|
|
138
|
+
gem 'hanikamu-operation', '~> 0.2.0'
|
|
151
139
|
```
|
|
152
140
|
|
|
153
141
|
```bash
|
|
@@ -214,68 +202,7 @@ brew services start redis
|
|
|
214
202
|
REDIS_URL=redis://localhost:6379/0
|
|
215
203
|
```
|
|
216
204
|
|
|
217
|
-
**Step 5:
|
|
218
|
-
|
|
219
|
-
```bash
|
|
220
|
-
# Create operations directory
|
|
221
|
-
mkdir -p app/operations/users
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
```ruby
|
|
225
|
-
# app/operations/users/create_user_operation.rb
|
|
226
|
-
module Users
|
|
227
|
-
class CreateUserOperation < Hanikamu::Operation
|
|
228
|
-
attribute :email, Types::String
|
|
229
|
-
attribute :password, Types::String
|
|
230
|
-
|
|
231
|
-
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
232
|
-
validates :password, length: { minimum: 8 }
|
|
233
|
-
|
|
234
|
-
within_transaction(:base)
|
|
235
|
-
|
|
236
|
-
def execute
|
|
237
|
-
user = User.create!(
|
|
238
|
-
email: email,
|
|
239
|
-
password: password
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
response user: user
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
**Step 6: Use in your controller**
|
|
249
|
-
|
|
250
|
-
```ruby
|
|
251
|
-
# app/controllers/users_controller.rb
|
|
252
|
-
class UsersController < ApplicationController
|
|
253
|
-
def create
|
|
254
|
-
result = Users::CreateUserOperation.call(user_params)
|
|
255
|
-
|
|
256
|
-
if result.success?
|
|
257
|
-
user = result.success.user
|
|
258
|
-
render json: { user: user }, status: :created
|
|
259
|
-
else
|
|
260
|
-
error = result.failure
|
|
261
|
-
case error
|
|
262
|
-
when Hanikamu::Operation::FormError
|
|
263
|
-
render json: { errors: error.errors.full_messages }, status: :unprocessable_entity
|
|
264
|
-
else
|
|
265
|
-
render json: { error: error.message }, status: :internal_server_error
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
private
|
|
271
|
-
|
|
272
|
-
def user_params
|
|
273
|
-
params.require(:user).permit(:email, :password)
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
**Step 7: Configure for production**
|
|
205
|
+
**Step 5: Production configuration**
|
|
279
206
|
|
|
280
207
|
Set your Redis URL in production (Heroku, AWS, etc.):
|
|
281
208
|
|
|
@@ -284,71 +211,11 @@ Set your Redis URL in production (Heroku, AWS, etc.):
|
|
|
284
211
|
heroku addons:create heroku-redis:mini
|
|
285
212
|
# REDIS_URL is automatically set
|
|
286
213
|
|
|
287
|
-
# Or set manually
|
|
214
|
+
# Or set manually for other providers
|
|
288
215
|
heroku config:set REDIS_URL=redis://your-redis-host:6379/0
|
|
289
216
|
```
|
|
290
217
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
If you need more control, create a detailed initializer:
|
|
294
|
-
|
|
295
|
-
```ruby
|
|
296
|
-
# config/initializers/hanikamu_operation.rb
|
|
297
|
-
require 'redis-client'
|
|
298
|
-
|
|
299
|
-
Hanikamu::Operation.configure do |config|
|
|
300
|
-
# Required: Redis client for distributed locking
|
|
301
|
-
config.redis_client = RedisClient.new(
|
|
302
|
-
url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0'),
|
|
303
|
-
reconnect_attempts: 3,
|
|
304
|
-
timeout: 1.0
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
# Optional: Customize Redlock settings (these are the defaults)
|
|
308
|
-
config.mutex_expire_milliseconds = 1500 # Lock TTL
|
|
309
|
-
config.redlock_retry_count = 6 # Number of retry attempts
|
|
310
|
-
config.redlock_retry_delay = 500 # Milliseconds between retries
|
|
311
|
-
config.redlock_retry_jitter = 50 # Random jitter to prevent thundering herd
|
|
312
|
-
config.redlock_timeout = 0.1 # Redis command timeout
|
|
313
|
-
|
|
314
|
-
# Optional: Add errors to whitelist (Redlock::LockError is always included by default)
|
|
315
|
-
config.whitelisted_errors = [CustomBusinessError]
|
|
316
|
-
end
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### Redis Setup by Environment
|
|
320
|
-
|
|
321
|
-
**For Development (Docker Compose)**:
|
|
322
|
-
|
|
323
|
-
```yaml
|
|
324
|
-
# docker-compose.yml
|
|
325
|
-
services:
|
|
326
|
-
redis:
|
|
327
|
-
image: redis:7-alpine
|
|
328
|
-
volumes:
|
|
329
|
-
- redis_data:/data
|
|
330
|
-
networks:
|
|
331
|
-
- app_network
|
|
332
|
-
|
|
333
|
-
app:
|
|
334
|
-
# ... your app config
|
|
335
|
-
environment:
|
|
336
|
-
REDIS_URL: redis://redis:6379/0
|
|
337
|
-
depends_on:
|
|
338
|
-
- redis
|
|
339
|
-
networks:
|
|
340
|
-
- app_network
|
|
341
|
-
|
|
342
|
-
volumes:
|
|
343
|
-
redis_data:
|
|
344
|
-
|
|
345
|
-
networks:
|
|
346
|
-
app_network:
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
**For Production**:
|
|
350
|
-
|
|
351
|
-
Use a managed Redis service (AWS ElastiCache, Heroku Redis, Redis Labs, etc.) and set the `REDIS_URL` environment variable.
|
|
218
|
+
For advanced configuration options, see the [Configuration Reference](#configuration-reference) section below.
|
|
352
219
|
|
|
353
220
|
## Usage
|
|
354
221
|
|
|
@@ -465,6 +332,38 @@ def mutex_lock
|
|
|
465
332
|
end
|
|
466
333
|
```
|
|
467
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
|
+
|
|
468
367
|
### Database Transactions with `within_transaction`
|
|
469
368
|
|
|
470
369
|
Ensure multiple database changes succeed or fail together atomically. If any database operation raises an exception, all changes are rolled back.
|
|
@@ -764,52 +663,68 @@ Operations validate at three distinct levels, each serving a specific purpose:
|
|
|
764
663
|
|
|
765
664
|
### FormError vs GuardError: Practical Examples
|
|
766
665
|
|
|
767
|
-
|
|
666
|
+
Here's a complete example demonstrating the difference between form validations and guard conditions:
|
|
768
667
|
|
|
769
668
|
```ruby
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
#
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
669
|
+
class TestOperation < Hanikamu::Operation
|
|
670
|
+
attribute :sentence, Types::String
|
|
671
|
+
|
|
672
|
+
# Form validation - checks input value format/content
|
|
673
|
+
validates :sentence, presence: true
|
|
674
|
+
validates :sentence, exclusion: { in: ["form_error"], message: "is not allowed" }
|
|
675
|
+
|
|
676
|
+
# Guard validation - checks business rules/state
|
|
677
|
+
guard do
|
|
678
|
+
delegates :sentence
|
|
679
|
+
|
|
680
|
+
validates :sentence, exclusion: { in: ["guard_error"], message: "is not allowed" }
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
within_mutex(:mutex_lock)
|
|
684
|
+
within_transaction(:base)
|
|
685
|
+
|
|
686
|
+
def execute
|
|
687
|
+
response(message: sentence.reverse)
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def mutex_lock
|
|
691
|
+
self.class.name
|
|
692
|
+
end
|
|
693
|
+
end
|
|
788
694
|
```
|
|
789
695
|
|
|
790
|
-
**
|
|
696
|
+
**FormError Example** - Invalid input value triggers form validation:
|
|
791
697
|
|
|
792
698
|
```ruby
|
|
793
|
-
#
|
|
794
|
-
result =
|
|
795
|
-
# =>
|
|
699
|
+
# Form validation fails - input itself is invalid
|
|
700
|
+
result = TestOperation.call(sentence: "form_error")
|
|
701
|
+
# => Failure(#<Hanikamu::Operation::FormError: Sentence is not allowed>)
|
|
702
|
+
|
|
703
|
+
# Correcting the input allows the operation to proceed
|
|
704
|
+
result = TestOperation.call(sentence: "hello world")
|
|
705
|
+
# => Success(#<struct message="dlrow olleh">)
|
|
706
|
+
```
|
|
796
707
|
|
|
797
|
-
|
|
798
|
-
result = Users::CompleteUserOperation.call!(user_id: 46)
|
|
799
|
-
# => Failure(#<Hanikamu::Operation::GuardError: User has already been completed>)
|
|
708
|
+
**GuardError Example** - Valid input but business rule violated:
|
|
800
709
|
|
|
801
|
-
|
|
802
|
-
#
|
|
710
|
+
```ruby
|
|
711
|
+
# Input passes form validation, but guard fails
|
|
712
|
+
result = TestOperation.call(sentence: "guard_error")
|
|
713
|
+
# => Failure(#<Hanikamu::Operation::GuardError: Sentence is not allowed>)
|
|
714
|
+
|
|
715
|
+
# The input format is valid, but the business rule prevents execution
|
|
803
716
|
```
|
|
804
717
|
|
|
805
718
|
**Type Error Example** - Wrong argument type:
|
|
806
719
|
|
|
807
720
|
```ruby
|
|
808
|
-
# Passing wrong type raises immediately
|
|
809
|
-
|
|
721
|
+
# Passing wrong type raises immediately before any validations
|
|
722
|
+
TestOperation.call!(sentence: 123)
|
|
810
723
|
# => Raises Dry::Struct::Error
|
|
811
724
|
```
|
|
812
725
|
|
|
726
|
+
**Key Insight**: Form validations check if the *input is correct*, while guards check if the *operation can proceed* given the current state.
|
|
727
|
+
|
|
813
728
|
### Using `.call!` (Raises Exceptions)
|
|
814
729
|
|
|
815
730
|
```ruby
|
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
|
|
@@ -132,15 +131,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
132
131
|
requirements:
|
|
133
132
|
- - ">="
|
|
134
133
|
- !ruby/object:Gem::Version
|
|
135
|
-
version: 3.
|
|
134
|
+
version: 3.4.0
|
|
136
135
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
136
|
requirements:
|
|
138
137
|
- - ">="
|
|
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: []
|