cloudtasker 0.2.0 → 0.3.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/.gitignore +4 -0
- data/.rubocop.yml +5 -0
- data/.travis.yml +10 -1
- data/Appraisals +25 -0
- data/Gemfile.lock +10 -4
- data/README.md +550 -4
- data/app/controllers/cloudtasker/application_controller.rb +2 -0
- data/app/controllers/cloudtasker/worker_controller.rb +22 -2
- data/cloudtasker.gemspec +4 -3
- data/docs/BATCH_JOBS.md +66 -0
- data/docs/CRON_JOBS.md +63 -0
- data/docs/UNIQUE_JOBS.md +127 -0
- data/exe/cloudtasker +15 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/google_cloud_tasks_1.0.gemfile +9 -0
- data/gemfiles/google_cloud_tasks_1.0.gemfile.lock +263 -0
- data/gemfiles/google_cloud_tasks_1.1.gemfile +9 -0
- data/gemfiles/google_cloud_tasks_1.1.gemfile.lock +263 -0
- data/gemfiles/google_cloud_tasks_1.2.gemfile +9 -0
- data/gemfiles/google_cloud_tasks_1.2.gemfile.lock +263 -0
- data/gemfiles/google_cloud_tasks_1.3.gemfile +9 -0
- data/gemfiles/google_cloud_tasks_1.3.gemfile.lock +264 -0
- data/gemfiles/rails_4.0.gemfile +10 -0
- data/gemfiles/rails_4.1.gemfile +9 -0
- data/gemfiles/rails_4.2.gemfile +9 -0
- data/gemfiles/rails_5.0.gemfile +9 -0
- data/gemfiles/rails_5.1.gemfile +9 -0
- data/gemfiles/rails_5.2.gemfile +9 -0
- data/gemfiles/rails_5.2.gemfile.lock +247 -0
- data/gemfiles/rails_6.0.gemfile +9 -0
- data/gemfiles/rails_6.0.gemfile.lock +263 -0
- data/lib/cloudtasker.rb +19 -1
- data/lib/cloudtasker/backend/google_cloud_task.rb +139 -0
- data/lib/cloudtasker/backend/memory_task.rb +190 -0
- data/lib/cloudtasker/backend/redis_task.rb +248 -0
- data/lib/cloudtasker/batch/batch_progress.rb +19 -1
- data/lib/cloudtasker/batch/job.rb +81 -20
- data/lib/cloudtasker/cli.rb +194 -0
- data/lib/cloudtasker/cloud_task.rb +91 -0
- data/lib/cloudtasker/config.rb +64 -2
- data/lib/cloudtasker/cron/job.rb +2 -2
- data/lib/cloudtasker/cron/schedule.rb +15 -5
- data/lib/cloudtasker/dead_worker_error.rb +6 -0
- data/lib/cloudtasker/local_server.rb +74 -0
- data/lib/cloudtasker/railtie.rb +10 -0
- data/lib/cloudtasker/testing.rb +133 -0
- data/lib/cloudtasker/unique_job/job.rb +1 -1
- data/lib/cloudtasker/unique_job/lock/base_lock.rb +1 -1
- data/lib/cloudtasker/unique_job/lock/until_executed.rb +3 -1
- data/lib/cloudtasker/unique_job/lock/while_executing.rb +3 -1
- data/lib/cloudtasker/version.rb +1 -1
- data/lib/cloudtasker/worker.rb +59 -16
- data/lib/cloudtasker/{task.rb → worker_handler.rb} +10 -77
- data/lib/cloudtasker/worker_logger.rb +155 -0
- data/lib/tasks/setup_queue.rake +10 -0
- metadata +55 -6
|
@@ -9,6 +9,9 @@ module Cloudtasker
|
|
|
9
9
|
# Key Namespace used for object saved under this class
|
|
10
10
|
SUB_NAMESPACE = 'job'
|
|
11
11
|
|
|
12
|
+
# List of statuses triggering a completion callback
|
|
13
|
+
COMPLETION_STATUSES = %w[completed dead].freeze
|
|
14
|
+
|
|
12
15
|
#
|
|
13
16
|
# Return the cloudtasker redis client
|
|
14
17
|
#
|
|
@@ -28,13 +31,13 @@ module Cloudtasker
|
|
|
28
31
|
def self.find(worker_id)
|
|
29
32
|
return nil unless worker_id
|
|
30
33
|
|
|
31
|
-
# Retrieve
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return nil unless
|
|
34
|
+
# Retrieve related worker
|
|
35
|
+
payload = redis.fetch(key(worker_id))
|
|
36
|
+
worker = Cloudtasker::Worker.from_hash(payload)
|
|
37
|
+
return nil unless worker
|
|
35
38
|
|
|
36
39
|
# Build batch job
|
|
37
|
-
self.for(
|
|
40
|
+
self.for(worker)
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
#
|
|
@@ -223,24 +226,62 @@ module Cloudtasker
|
|
|
223
226
|
return true unless state
|
|
224
227
|
|
|
225
228
|
# Check that all children are complete
|
|
226
|
-
state.values.all? { |e| e
|
|
229
|
+
state.values.all? { |e| COMPLETION_STATUSES.include?(e) }
|
|
227
230
|
end
|
|
228
231
|
end
|
|
229
232
|
|
|
233
|
+
#
|
|
234
|
+
# Run worker callback in a controlled environment to
|
|
235
|
+
# avoid interruption of the callback flow.
|
|
236
|
+
#
|
|
237
|
+
# @param [String, Symbol] callback The callback to run.
|
|
238
|
+
# @param [Array<any>] *args The callback arguments.
|
|
239
|
+
#
|
|
240
|
+
# @return [any] The callback return value
|
|
241
|
+
#
|
|
242
|
+
def run_worker_callback(callback, *args)
|
|
243
|
+
worker.try(callback, *args)
|
|
244
|
+
rescue StandardError => e
|
|
245
|
+
Cloudtasker.logger.error("Error running callback #{callback}: #{e}")
|
|
246
|
+
Cloudtasker.logger.error(e.backtrace.join("\n"))
|
|
247
|
+
nil
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
#
|
|
251
|
+
# Callback invoked when the batch is complete
|
|
252
|
+
#
|
|
253
|
+
def on_complete(status = :completed)
|
|
254
|
+
# Invoke worker callback
|
|
255
|
+
run_worker_callback(:on_batch_complete) if status == :completed
|
|
256
|
+
|
|
257
|
+
# Propagate event
|
|
258
|
+
parent_batch&.on_child_complete(self, status)
|
|
259
|
+
ensure
|
|
260
|
+
# The batch tree is complete. Cleanup the tree.
|
|
261
|
+
cleanup unless parent_batch
|
|
262
|
+
end
|
|
263
|
+
|
|
230
264
|
#
|
|
231
265
|
# Callback invoked when a direct child batch is complete.
|
|
232
266
|
#
|
|
233
267
|
# @param [Cloudtasker::Batch::Job] child_batch The completed child batch.
|
|
234
268
|
#
|
|
235
|
-
def on_child_complete(child_batch)
|
|
269
|
+
def on_child_complete(child_batch, status = :completed)
|
|
236
270
|
# Update batch state
|
|
237
|
-
update_state(child_batch.batch_id,
|
|
271
|
+
update_state(child_batch.batch_id, status)
|
|
238
272
|
|
|
239
273
|
# Notify the worker that a direct batch child worker has completed
|
|
240
|
-
|
|
274
|
+
case status
|
|
275
|
+
when :completed
|
|
276
|
+
run_worker_callback(:on_child_complete, child_batch.worker)
|
|
277
|
+
when :errored
|
|
278
|
+
run_worker_callback(:on_child_error, child_batch.worker)
|
|
279
|
+
when :dead
|
|
280
|
+
run_worker_callback(:on_child_dead, child_batch.worker)
|
|
281
|
+
end
|
|
241
282
|
|
|
242
283
|
# Notify the parent batch that we are done with this batch
|
|
243
|
-
|
|
284
|
+
on_complete if status != :errored && complete?
|
|
244
285
|
end
|
|
245
286
|
|
|
246
287
|
#
|
|
@@ -248,14 +289,31 @@ module Cloudtasker
|
|
|
248
289
|
#
|
|
249
290
|
# @param [Cloudtasker::Batch::Job] child_batch The completed child batch.
|
|
250
291
|
#
|
|
251
|
-
def on_batch_node_complete(child_batch)
|
|
292
|
+
def on_batch_node_complete(child_batch, status = :completed)
|
|
293
|
+
return false unless status == :completed
|
|
294
|
+
|
|
252
295
|
# Notify the worker that a batch node worker has completed
|
|
253
|
-
|
|
296
|
+
run_worker_callback(:on_batch_node_complete, child_batch.worker)
|
|
254
297
|
|
|
255
298
|
# Notify the parent batch that a node is complete
|
|
256
299
|
parent_batch&.on_batch_node_complete(child_batch)
|
|
257
300
|
end
|
|
258
301
|
|
|
302
|
+
#
|
|
303
|
+
# Remove all batch and sub-batch keys from Redis.
|
|
304
|
+
#
|
|
305
|
+
def cleanup
|
|
306
|
+
# Capture batch state
|
|
307
|
+
state = batch_state
|
|
308
|
+
|
|
309
|
+
# Delete child batches recursively
|
|
310
|
+
state.to_h.keys.each { |id| self.class.find(id)&.cleanup }
|
|
311
|
+
|
|
312
|
+
# Delete batch redis entries
|
|
313
|
+
redis.del(batch_gid)
|
|
314
|
+
redis.del(batch_state_gid)
|
|
315
|
+
end
|
|
316
|
+
|
|
259
317
|
#
|
|
260
318
|
# Calculate the progress of the batch.
|
|
261
319
|
#
|
|
@@ -274,7 +332,7 @@ module Cloudtasker
|
|
|
274
332
|
#
|
|
275
333
|
# Save the batch and enqueue all child workers attached to it.
|
|
276
334
|
#
|
|
277
|
-
# @return [Array<
|
|
335
|
+
# @return [Array<Cloudtasker::CloudTask>] The Google Task responses
|
|
278
336
|
#
|
|
279
337
|
def setup
|
|
280
338
|
return true if jobs.empty?
|
|
@@ -289,17 +347,14 @@ module Cloudtasker
|
|
|
289
347
|
#
|
|
290
348
|
# Post-perform logic. The parent batch is notified if the job is complete.
|
|
291
349
|
#
|
|
292
|
-
def complete
|
|
350
|
+
def complete(status = :completed)
|
|
293
351
|
return true if reenqueued? || jobs.any?
|
|
294
352
|
|
|
295
353
|
# Notify the parent batch that a child is complete
|
|
296
|
-
if complete?
|
|
297
|
-
worker.try(:on_batch_complete)
|
|
298
|
-
parent_batch&.on_child_complete(self)
|
|
299
|
-
end
|
|
354
|
+
on_complete(status) if complete?
|
|
300
355
|
|
|
301
356
|
# Notify the parent that a batch node has completed
|
|
302
|
-
parent_batch&.on_batch_node_complete(self)
|
|
357
|
+
parent_batch&.on_batch_node_complete(self, status)
|
|
303
358
|
end
|
|
304
359
|
|
|
305
360
|
#
|
|
@@ -316,7 +371,13 @@ module Cloudtasker
|
|
|
316
371
|
setup
|
|
317
372
|
|
|
318
373
|
# Complete batch
|
|
319
|
-
complete
|
|
374
|
+
complete(:success)
|
|
375
|
+
rescue DeadWorkerError => e
|
|
376
|
+
complete(:dead)
|
|
377
|
+
raise(e)
|
|
378
|
+
rescue StandardError => e
|
|
379
|
+
complete(:errored)
|
|
380
|
+
raise(e)
|
|
320
381
|
end
|
|
321
382
|
end
|
|
322
383
|
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cloudtasker'
|
|
4
|
+
require 'cloudtasker/local_server'
|
|
5
|
+
|
|
6
|
+
module Cloudtasker
|
|
7
|
+
# Cloudtasker executable logic
|
|
8
|
+
module CLI
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
#
|
|
12
|
+
# Return the current environment.
|
|
13
|
+
#
|
|
14
|
+
# @return [String] The environment name.
|
|
15
|
+
#
|
|
16
|
+
def environment
|
|
17
|
+
Cloudtasker.config.environment
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#
|
|
21
|
+
# Return true if we are running in Rails.
|
|
22
|
+
#
|
|
23
|
+
# @return [Boolean] True if rails is loaded.
|
|
24
|
+
#
|
|
25
|
+
def rails_app?
|
|
26
|
+
defined?(::Rails)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# Return true if we are running in JRuby.
|
|
31
|
+
#
|
|
32
|
+
# @return [Boolean] True if JRuby is loaded.
|
|
33
|
+
#
|
|
34
|
+
def jruby?
|
|
35
|
+
defined?(::JRUBY_VERSION)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# Return the Cloudtasker logger
|
|
40
|
+
#
|
|
41
|
+
# @return [Logger] The Cloudtasker logger.
|
|
42
|
+
#
|
|
43
|
+
def logger
|
|
44
|
+
Cloudtasker.logger
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#
|
|
48
|
+
# Return the local Cloudtasker server.
|
|
49
|
+
#
|
|
50
|
+
# @return [Cloudtasker::LocalServer] The local Cloudtasker server.
|
|
51
|
+
#
|
|
52
|
+
def local_server
|
|
53
|
+
@local_server ||= LocalServer.new
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#
|
|
57
|
+
# Load Rails if defined
|
|
58
|
+
#
|
|
59
|
+
def boot_system
|
|
60
|
+
# Sync logs
|
|
61
|
+
STDOUT.sync = true
|
|
62
|
+
|
|
63
|
+
# Check for Rails
|
|
64
|
+
return false unless File.exist?('./config/environment.rb')
|
|
65
|
+
|
|
66
|
+
require 'rails'
|
|
67
|
+
require File.expand_path('./config/environment.rb')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
#
|
|
71
|
+
# Run the cloudtasker development server.
|
|
72
|
+
#
|
|
73
|
+
def run
|
|
74
|
+
boot_system
|
|
75
|
+
|
|
76
|
+
# Print banner
|
|
77
|
+
environment == 'development' ? print_banner : print_non_dev_warning
|
|
78
|
+
|
|
79
|
+
# Print rails info
|
|
80
|
+
if rails_app?
|
|
81
|
+
logger.info "[Cloudtasker/Server] Booted Rails #{::Rails.version} application in #{environment} environment"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get internal read/write pip
|
|
85
|
+
self_read, self_write = IO.pipe
|
|
86
|
+
|
|
87
|
+
# Setup signals to trap
|
|
88
|
+
setup_signals(self_write)
|
|
89
|
+
|
|
90
|
+
logger.info "[Cloudtasker/Server] Running in #{RUBY_DESCRIPTION}"
|
|
91
|
+
|
|
92
|
+
# Wait for signals
|
|
93
|
+
wait_for_signal(self_read)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
#
|
|
97
|
+
# Wait for signals and handle them.
|
|
98
|
+
#
|
|
99
|
+
# @param [IO] read_pipe Where to read signals.
|
|
100
|
+
#
|
|
101
|
+
def wait_for_signal(read_pipe)
|
|
102
|
+
local_server.start
|
|
103
|
+
|
|
104
|
+
while (readable_io = IO.select([read_pipe]))
|
|
105
|
+
signal = readable_io.first[0].gets.strip
|
|
106
|
+
handle_signal(signal)
|
|
107
|
+
end
|
|
108
|
+
rescue Interrupt
|
|
109
|
+
logger.info 'Shutting down'
|
|
110
|
+
local_server.stop
|
|
111
|
+
logger.info 'Stopped'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
#
|
|
115
|
+
# Define which signals to trap
|
|
116
|
+
#
|
|
117
|
+
# @param [IO] write_pipe Where to write signals.
|
|
118
|
+
#
|
|
119
|
+
def setup_signals(write_pipe)
|
|
120
|
+
# Display signals on log output
|
|
121
|
+
sigs = %w[INT TERM TTIN TSTP]
|
|
122
|
+
# USR1 and USR2 don't work on the JVM
|
|
123
|
+
sigs << 'USR2' unless jruby?
|
|
124
|
+
sigs.each do |sig|
|
|
125
|
+
begin
|
|
126
|
+
trap(sig) { write_pipe.puts(sig) }
|
|
127
|
+
rescue ArgumentError
|
|
128
|
+
puts "Signal #{sig} not supported"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
#
|
|
134
|
+
# Handle process signals
|
|
135
|
+
#
|
|
136
|
+
# @param [String] sig The signal.
|
|
137
|
+
#
|
|
138
|
+
def handle_signal(sig)
|
|
139
|
+
raise(Interrupt) if %w[INT TERM].include?(sig)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
#
|
|
143
|
+
# Return the server banner
|
|
144
|
+
#
|
|
145
|
+
# @return [String] The server banner
|
|
146
|
+
#
|
|
147
|
+
def banner
|
|
148
|
+
<<~'TEXT'
|
|
149
|
+
___ _ _ _ _
|
|
150
|
+
/ __\ | ___ _ _ __| | |_ __ _ ___| | _____ _ __
|
|
151
|
+
/ / | |/ _ \| | | |/ _` | __/ _` / __| |/ / _ \ '__|
|
|
152
|
+
/ /___| | (_) | |_| | (_| | || (_| \__ \ < __/ |
|
|
153
|
+
\____/|_|\___/ \__,_|\__,_|\__\__,_|___/_|\_\___|_|
|
|
154
|
+
|
|
155
|
+
TEXT
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
#
|
|
159
|
+
# Display a warning message when run in non-dev env.
|
|
160
|
+
#
|
|
161
|
+
# @return [<Type>] <description>
|
|
162
|
+
#
|
|
163
|
+
def print_non_dev_warning
|
|
164
|
+
puts "\e[31m"
|
|
165
|
+
puts non_dev_warning_message
|
|
166
|
+
puts "\e[0m"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
#
|
|
170
|
+
# Return the message to display when users attempt to run
|
|
171
|
+
# the local development server in non-dev environments.
|
|
172
|
+
#
|
|
173
|
+
# @return [String] The warning message.
|
|
174
|
+
#
|
|
175
|
+
def non_dev_warning_message
|
|
176
|
+
<<~'TEXT'
|
|
177
|
+
============================================ /!\ ====================================================
|
|
178
|
+
Your are running the Cloudtasker local development server in a NON-DEVELOPMENT environment.
|
|
179
|
+
This is not recommended as the the development server is not designed for production-like load.
|
|
180
|
+
If you need a job processing server to run yourself please use Sidekiq instead (https://sidekiq.org)
|
|
181
|
+
============================================ /!\ ====================================================
|
|
182
|
+
TEXT
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
#
|
|
186
|
+
# Print the server banner
|
|
187
|
+
#
|
|
188
|
+
def print_banner
|
|
189
|
+
puts "\e[96m"
|
|
190
|
+
puts banner
|
|
191
|
+
puts "\e[0m"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudtasker
|
|
4
|
+
# An interface class to manage tasks on the backend (Cloud Task or Redis)
|
|
5
|
+
class CloudTask
|
|
6
|
+
attr_accessor :id, :http_request, :schedule_time, :retries
|
|
7
|
+
|
|
8
|
+
#
|
|
9
|
+
# The backend to use for cloud tasks.
|
|
10
|
+
#
|
|
11
|
+
# @return [Cloudtasker::Backend::GoogleCloudTask, Cloudtasker::Backend::RedisTask] The cloud task backend.
|
|
12
|
+
#
|
|
13
|
+
def self.backend
|
|
14
|
+
# Re-evaluate backend every time if testing mode enabled
|
|
15
|
+
@backend = nil if defined?(Cloudtasker::Testing)
|
|
16
|
+
|
|
17
|
+
@backend ||= begin
|
|
18
|
+
if defined?(Cloudtasker::Testing) && Cloudtasker::Testing.in_memory?
|
|
19
|
+
require 'cloudtasker/backend/memory_task'
|
|
20
|
+
Backend::MemoryTask
|
|
21
|
+
elsif Cloudtasker.config.mode.to_sym == :development
|
|
22
|
+
require 'cloudtasker/backend/redis_task'
|
|
23
|
+
Backend::RedisTask
|
|
24
|
+
else
|
|
25
|
+
require 'cloudtasker/backend/google_cloud_task'
|
|
26
|
+
Backend::GoogleCloudTask
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#
|
|
32
|
+
# Find a cloud task by id.
|
|
33
|
+
#
|
|
34
|
+
# @param [String] id The id of the task.
|
|
35
|
+
#
|
|
36
|
+
# @return [Cloudtasker::Cloudtask] The task.
|
|
37
|
+
#
|
|
38
|
+
def self.find(id)
|
|
39
|
+
payload = backend.find(id)&.to_h
|
|
40
|
+
payload ? new(payload) : nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
#
|
|
44
|
+
# Create a new cloud task.
|
|
45
|
+
#
|
|
46
|
+
# @param [Hash] payload Thee task payload
|
|
47
|
+
#
|
|
48
|
+
# @return [Cloudtasker::CloudTask] The created task.
|
|
49
|
+
#
|
|
50
|
+
def self.create(payload)
|
|
51
|
+
resp = backend.create(payload)&.to_h
|
|
52
|
+
resp ? new(resp) : nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#
|
|
56
|
+
# Delete a cloud task by id.
|
|
57
|
+
#
|
|
58
|
+
# @param [String] id The task id.
|
|
59
|
+
#
|
|
60
|
+
def self.delete(id)
|
|
61
|
+
backend.delete(id)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#
|
|
65
|
+
# Build a new instance of the class using a backend response
|
|
66
|
+
# payload.
|
|
67
|
+
#
|
|
68
|
+
# @param [String] id The task id.
|
|
69
|
+
# @param [Hash] http_request The content of the http request.
|
|
70
|
+
# @param [Integer] schedule_time When to run the job (Unix timestamp)
|
|
71
|
+
# @param [Integer] retries The number of times the job failed.
|
|
72
|
+
#
|
|
73
|
+
def initialize(id:, http_request:, schedule_time: nil, retries: 0)
|
|
74
|
+
@id = id
|
|
75
|
+
@http_request = http_request
|
|
76
|
+
@schedule_time = schedule_time
|
|
77
|
+
@retries = retries || 0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#
|
|
81
|
+
# Equality operator.
|
|
82
|
+
#
|
|
83
|
+
# @param [Any] other The object to compare.
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean] True if the object is equal.
|
|
86
|
+
#
|
|
87
|
+
def ==(other)
|
|
88
|
+
other.is_a?(self.class) && other.id == id
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|