shoryuken 7.0.0.alpha2 → 7.0.0.rc1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 962cd757e952228ae34b93d4fa479c623a2cc60e84aa15bbd40645546bb58a59
4
- data.tar.gz: 2313131f61b7299be011498977450cd8eb06392fa176d095111a7195a86eb033
3
+ metadata.gz: 44ec6add623605d696f0a4151aab96d37331250fa66a6b804edea13b06a2b816
4
+ data.tar.gz: efe2733bb86113a83b7788041262421c84ee4c50a47718b32ef8bdb505fa15fe
5
5
  SHA512:
6
- metadata.gz: ffc81ebcf932be68803e4745bdd0070c67f65bfd1a638ef455cb4339d3f5dc3803b11ff11de9bb2892f3205ec7cff81ce2f807700fa3f0869a683ffc9255541e
7
- data.tar.gz: d4881cd3e103f290bbfefa48e4d20d8537e4d4aa32d269567bf18b1a77545fc188cc4fe90876a7053c29c527ff94d614d46713e504f7f0f6d117ee25d0a6e779
6
+ metadata.gz: 84daafb107ff842e74cdb3872885b6c01ea34d95a5eaf6cf2efeeb3398bc835e570ea5b7f93b9451e4e5841d1d27b53ea35db677a4ddd78cdd75c13b417eebae
7
+ data.tar.gz: ad0dcb19240b8179c62bb6b74e23b1801d1f0d671a0f72a5b26d74a0bbd1f47158abd57a6cee091d88fe903ea91ce09afd34702f9086cb4b2cf14a9a17397509
@@ -45,20 +45,17 @@ jobs:
45
45
  name: Rails Specs
46
46
  strategy:
47
47
  matrix:
48
- rails: ['7.0', '7.1', '7.2', '8.0']
48
+ rails: ['7.2', '8.0', '8.1']
49
49
  include:
50
- - rails: '7.0'
51
- ruby: '3.1'
52
- gemfile: gemfiles/rails_7_0.gemfile
53
- - rails: '7.1'
54
- ruby: '3.2'
55
- gemfile: gemfiles/rails_7_1.gemfile
56
50
  - rails: '7.2'
57
- ruby: '3.3'
51
+ ruby: '3.2'
58
52
  gemfile: gemfiles/rails_7_2.gemfile
59
53
  - rails: '8.0'
60
- ruby: '3.4'
54
+ ruby: '3.3'
61
55
  gemfile: gemfiles/rails_8_0.gemfile
56
+ - rails: '8.1'
57
+ ruby: '3.4'
58
+ gemfile: gemfiles/rails_8_1.gemfile
62
59
  runs-on: ubuntu-latest
63
60
  env:
64
61
  BUNDLE_GEMFILE: ${{ matrix.gemfile }}
@@ -73,3 +70,19 @@ jobs:
73
70
 
74
71
  - name: Run Rails specs
75
72
  run: bundle exec rake spec:rails
73
+
74
+ ci-success:
75
+ name: CI Success
76
+ runs-on: ubuntu-latest
77
+ if: always()
78
+ needs:
79
+ - all_specs
80
+ - rails_specs
81
+ steps:
82
+ - name: Check all jobs passed
83
+ if: |
84
+ contains(needs.*.result, 'failure') ||
85
+ contains(needs.*.result, 'cancelled') ||
86
+ contains(needs.*.result, 'skipped')
87
+ run: exit 1
88
+ - run: echo "All CI checks passed!"
data/Appraisals CHANGED
@@ -1,15 +1,3 @@
1
- appraise 'rails_7_0' do
2
- group :test do
3
- gem 'activejob', '~> 7.0'
4
- end
5
- end
6
-
7
- appraise 'rails_7_1' do
8
- group :test do
9
- gem 'activejob', '~> 7.1'
10
- end
11
- end
12
-
13
1
  appraise 'rails_7_2' do
14
2
  group :test do
15
3
  gem 'activejob', '~> 7.2'
@@ -21,3 +9,9 @@ appraise 'rails_8_0' do
21
9
  gem 'activejob', '~> 8.0'
22
10
  end
23
11
  end
12
+
13
+ appraise 'rails_8_1' do
14
+ group :test do
15
+ gem 'activejob', '~> 8.1'
16
+ end
17
+ end
data/CHANGELOG.md CHANGED
@@ -1,4 +1,18 @@
1
1
  ## [7.0.0] - Unreleased
2
+ - Enhancement: Add ActiveJob Continuations support (Rails 8.1+)
3
+ - Implements `stopping?` method in ActiveJob adapters to signal graceful shutdown
4
+ - Enables jobs to checkpoint progress and resume after interruption
5
+ - Handles past timestamps correctly (SQS treats negative delays as immediate delivery)
6
+ - Tracks shutdown state in Launcher via `stopping?` flag
7
+ - Leverages existing Shoryuken shutdown lifecycle (stop/stop! methods)
8
+ - Includes comprehensive integration tests with continuable jobs
9
+ - See Rails PR #55127 for more details on ActiveJob Continuations
10
+
11
+ - Breaking: Remove support for Rails versions older than 7.2
12
+ - Rails 7.0 and 7.1 have reached end-of-life and are no longer supported
13
+ - Supported versions: Rails 7.2, 8.0, and 8.1
14
+ - Users on older Rails versions should upgrade or remain on Shoryuken 6.x
15
+
2
16
  - Enhancement: Replace Concurrent::AtomicFixnum with pure Ruby AtomicCounter
3
17
  - Removes external dependency on concurrent-ruby for atomic fixnum operations
4
18
  - Introduces Shoryuken::Helpers::AtomicCounter as a thread-safe alternative using Mutex
data/bin/cli/sqs.rb CHANGED
@@ -13,7 +13,7 @@ module Shoryuken
13
13
  no_commands do
14
14
  def normalize_dump_message(message)
15
15
  # symbolize_keys is needed for keeping it compatible with `requeue`
16
- attributes = message[:attributes].symbolize_keys
16
+ attributes = message[:attributes].transform_keys(&:to_sym)
17
17
 
18
18
  # See https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html
19
19
  # The `string_list_values` and `binary_list_values` are not implemented. Reserved for future use.
data/bin/shoryuken CHANGED
@@ -29,7 +29,7 @@ module Shoryuken
29
29
  method_option :delay, aliases: '-D', type: :numeric,
30
30
  desc: 'Number of seconds to pause fetching from an empty queue'
31
31
  def start
32
- opts = options.to_h.symbolize_keys
32
+ opts = options.to_h.transform_keys(&:to_sym)
33
33
 
34
34
  say '[DEPRECATED] Please use --config instead of --config-file', :yellow if opts[:config_file]
35
35
 
@@ -1,19 +1,19 @@
1
1
  # This file was generated by Appraisal
2
2
 
3
- source 'https://rubygems.org'
3
+ source "https://rubygems.org"
4
4
 
5
5
  group :test do
6
- gem 'activejob', '~> 7.2'
7
- gem 'httparty'
8
- gem 'multi_xml'
9
- gem 'simplecov'
10
- gem 'warning'
6
+ gem "activejob", "~> 7.2"
7
+ gem "httparty"
8
+ gem "multi_xml"
9
+ gem "simplecov"
10
+ gem "warning"
11
11
  end
12
12
 
13
13
  group :development do
14
- gem 'appraisal', git: 'https://github.com/thoughtbot/appraisal.git'
15
- gem 'pry-byebug'
16
- gem 'rubocop'
14
+ gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
15
+ gem "pry-byebug"
16
+ gem "rubocop"
17
17
  end
18
18
 
19
- gemspec path: '../'
19
+ gemspec path: "../"
@@ -1,19 +1,19 @@
1
1
  # This file was generated by Appraisal
2
2
 
3
- source 'https://rubygems.org'
3
+ source "https://rubygems.org"
4
4
 
5
5
  group :test do
6
- gem 'activejob', '~> 8.0'
7
- gem 'httparty'
8
- gem 'multi_xml'
9
- gem 'simplecov'
10
- gem 'warning'
6
+ gem "activejob", "~> 8.0"
7
+ gem "httparty"
8
+ gem "multi_xml"
9
+ gem "simplecov"
10
+ gem "warning"
11
11
  end
12
12
 
13
13
  group :development do
14
- gem 'appraisal', git: 'https://github.com/thoughtbot/appraisal.git'
15
- gem 'pry-byebug'
16
- gem 'rubocop'
14
+ gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
15
+ gem "pry-byebug"
16
+ gem "rubocop"
17
17
  end
18
18
 
19
- gemspec path: '../'
19
+ gemspec path: "../"
@@ -0,0 +1,19 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ group :test do
6
+ gem "activejob", "~> 8.1"
7
+ gem "httparty"
8
+ gem "multi_xml"
9
+ gem "simplecov"
10
+ gem "warning"
11
+ end
12
+
13
+ group :development do
14
+ gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
15
+ gem "pry-byebug"
16
+ gem "rubocop"
17
+ end
18
+
19
+ gemspec path: "../"
@@ -37,6 +37,19 @@ module ActiveJob
37
37
  true
38
38
  end
39
39
 
40
+ # Indicates whether Shoryuken is in the process of shutting down.
41
+ #
42
+ # This method is required for ActiveJob Continuations support (Rails 8.1+).
43
+ # When true, it signals to jobs that they should checkpoint their progress
44
+ # and gracefully interrupt execution to allow for resumption after restart.
45
+ #
46
+ # @return [Boolean] true if Shoryuken is shutting down, false otherwise
47
+ # @see https://github.com/rails/rails/pull/55127 Rails ActiveJob Continuations
48
+ def stopping?
49
+ launcher = Shoryuken::Runner.instance.launcher
50
+ launcher&.stopping? || false
51
+ end
52
+
40
53
  def enqueue(job, options = {}) # :nodoc:
41
54
  register_worker!(job)
42
55
 
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Helpers
5
+ # A thread-safe timer task implementation.
6
+ # Drop-in replacement for Concurrent::TimerTask without external dependencies.
7
+ class TimerTask
8
+ def initialize(execution_interval:, &task)
9
+ raise ArgumentError, 'A block must be provided' unless block_given?
10
+
11
+ @execution_interval = Float(execution_interval)
12
+ raise ArgumentError, 'execution_interval must be positive' if @execution_interval <= 0
13
+
14
+ @task = task
15
+ @mutex = Mutex.new
16
+ @thread = nil
17
+ @running = false
18
+ @killed = false
19
+ end
20
+
21
+ # Start the timer task execution
22
+ def execute
23
+ @mutex.synchronize do
24
+ return self if @running || @killed
25
+
26
+ @running = true
27
+ @thread = Thread.new { run_timer_loop }
28
+ end
29
+ self
30
+ end
31
+
32
+ # Stop and kill the timer task
33
+ def kill
34
+ @mutex.synchronize do
35
+ return false if @killed
36
+
37
+ @killed = true
38
+ @running = false
39
+
40
+ @thread.kill if @thread&.alive?
41
+ end
42
+ true
43
+ end
44
+
45
+ private
46
+
47
+ def run_timer_loop
48
+ until @killed
49
+ sleep(@execution_interval)
50
+ break if @killed
51
+
52
+ begin
53
+ @task.call
54
+ rescue => e
55
+ # Log the error but continue running
56
+ # This matches the behavior of Concurrent::TimerTask
57
+ warn "TimerTask execution error: #{e.message}"
58
+ warn e.backtrace.join("\n") if e.backtrace
59
+ end
60
+ end
61
+ ensure
62
+ @mutex.synchronize { @running = false }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -6,6 +6,18 @@ module Shoryuken
6
6
 
7
7
  def initialize
8
8
  @managers = create_managers
9
+ @stopping = false
10
+ end
11
+
12
+ # Indicates whether the launcher is in the process of stopping.
13
+ #
14
+ # This flag is set to true when either {#stop} or {#stop!} is called,
15
+ # and is used by ActiveJob adapters to signal jobs that they should
16
+ # checkpoint and prepare for graceful shutdown.
17
+ #
18
+ # @return [Boolean] true if stopping, false otherwise
19
+ def stopping?
20
+ @stopping
9
21
  end
10
22
 
11
23
  def start
@@ -16,6 +28,7 @@ module Shoryuken
16
28
  end
17
29
 
18
30
  def stop!
31
+ @stopping = true
19
32
  initiate_stop
20
33
 
21
34
  # Don't await here so the timeout below is not delayed
@@ -28,6 +41,7 @@ module Shoryuken
28
41
  end
29
42
 
30
43
  def stop
44
+ @stopping = true
31
45
  fire_event(:quiet, true)
32
46
 
33
47
  initiate_stop
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Logging
5
+ # Base formatter class that provides common functionality for Shoryuken log formatters.
6
+ # Provides thread ID generation and context management.
7
+ class Base < ::Logger::Formatter
8
+ # Generates a thread ID for the current thread.
9
+ # Uses a combination of thread object_id and process ID to create a unique identifier.
10
+ #
11
+ # @return [String] A base36-encoded thread identifier
12
+ def tid
13
+ Thread.current['shoryuken_tid'] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
14
+ end
15
+
16
+ # Returns the current logging context as a formatted string.
17
+ # Context is set using {Shoryuken::Logging.with_context}.
18
+ #
19
+ # @return [String] Formatted context string or empty string if no context
20
+ def context
21
+ c = Thread.current[:shoryuken_context]
22
+ c ? " #{c}" : ''
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Logging
5
+ # A pretty log formatter that includes timestamps, process ID, thread ID,
6
+ # context information, and severity in a human-readable format.
7
+ #
8
+ # Output format: "TIMESTAMP PID TID-THREAD_ID CONTEXT SEVERITY: MESSAGE"
9
+ #
10
+ # @example Output
11
+ # 2023-08-15T10:30:45Z 12345 TID-abc123 MyWorker/queue1/msg-456 INFO: Processing message
12
+ class Pretty < Base
13
+ # Formats a log message with timestamp and full context information.
14
+ #
15
+ # @param severity [String] Log severity level (DEBUG, INFO, WARN, ERROR, FATAL)
16
+ # @param time [Time] Timestamp when the log entry was created
17
+ # @param _program_name [String] Program name (unused)
18
+ # @param message [String] The log message
19
+ # @return [String] Formatted log entry with newline
20
+ def call(severity, time, _program_name, message)
21
+ "#{time.utc.iso8601} #{Process.pid} TID-#{tid}#{context} #{severity}: #{message}\n"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Logging
5
+ # A log formatter that excludes timestamps from output.
6
+ # Useful for environments where timestamps are added by external logging systems.
7
+ #
8
+ # Output format: "pid=PID tid=THREAD_ID CONTEXT SEVERITY: MESSAGE"
9
+ #
10
+ # @example Output
11
+ # pid=12345 tid=abc123 MyWorker/queue1/msg-456 INFO: Processing message
12
+ class WithoutTimestamp < Base
13
+ # Formats a log message without timestamp information.
14
+ #
15
+ # @param severity [String] Log severity level (DEBUG, INFO, WARN, ERROR, FATAL)
16
+ # @param _time [Time] Timestamp (unused)
17
+ # @param _program_name [String] Program name (unused)
18
+ # @param message [String] The log message
19
+ # @return [String] Formatted log entry with newline
20
+ def call(severity, _time, _program_name, message)
21
+ "pid=#{Process.pid} tid=#{tid}#{context} #{severity}: #{message}\n"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -2,32 +2,12 @@
2
2
 
3
3
  require 'time'
4
4
  require 'logger'
5
+ require_relative 'logging/base'
6
+ require_relative 'logging/pretty'
7
+ require_relative 'logging/without_timestamp'
5
8
 
6
9
  module Shoryuken
7
10
  module Logging
8
- class Base < ::Logger::Formatter
9
- def tid
10
- Thread.current['shoryuken_tid'] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
11
- end
12
-
13
- def context
14
- c = Thread.current[:shoryuken_context]
15
- c ? " #{c}" : ''
16
- end
17
- end
18
-
19
- class Pretty < Base
20
- # Provide a call() method that returns the formatted message.
21
- def call(severity, time, _program_name, message)
22
- "#{time.utc.iso8601} #{Process.pid} TID-#{tid}#{context} #{severity}: #{message}\n"
23
- end
24
- end
25
-
26
- class WithoutTimestamp < Base
27
- def call(severity, _time, _program_name, message)
28
- "pid=#{Process.pid} tid=#{tid}#{context} #{severity}: #{message}\n"
29
- end
30
- end
31
11
 
32
12
  def self.with_context(msg)
33
13
  Thread.current[:shoryuken_context] = msg
@@ -1,9 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
+ # Represents an SQS message received by a Shoryuken worker.
5
+ # This class wraps the raw AWS SQS message data and provides convenient methods
6
+ # for interacting with the message, including deletion and visibility timeout management.
7
+ #
8
+ # Message instances are automatically created by Shoryuken and passed to your
9
+ # worker's `perform` method as the first argument.
10
+ #
11
+ # @example Basic worker with message handling
12
+ # class MyWorker
13
+ # include Shoryuken::Worker
14
+ # shoryuken_options queue: 'my_queue'
15
+ #
16
+ # def perform(sqs_msg, body)
17
+ # puts "Processing message #{sqs_msg.message_id}"
18
+ # puts "Message body: #{body}"
19
+ # puts "Queue: #{sqs_msg.queue_name}"
20
+ #
21
+ # # Process the message...
22
+ #
23
+ # # Delete the message when done (if auto_delete is false)
24
+ # sqs_msg.delete unless auto_delete?
25
+ # end
26
+ # end
27
+ #
28
+ # @example Working with message attributes
29
+ # def perform(sqs_msg, body)
30
+ # # Access standard SQS attributes
31
+ # sender_id = sqs_msg.attributes['SenderId']
32
+ # sent_timestamp = sqs_msg.attributes['SentTimestamp']
33
+ #
34
+ # # Access custom message attributes
35
+ # priority = sqs_msg.message_attributes['Priority']&.[]('StringValue')
36
+ # user_id = sqs_msg.message_attributes['UserId']&.[]('StringValue')
37
+ # end
4
38
  class Message
5
39
  extend Forwardable
6
40
 
41
+ # @!method message_id
42
+ # Returns the unique SQS message ID.
43
+ # @return [String] The message ID assigned by SQS
44
+ #
45
+ # @!method receipt_handle
46
+ # Returns the receipt handle needed for deleting or modifying the message.
47
+ # @return [String] The receipt handle for this message
48
+ #
49
+ # @!method md5_of_body
50
+ # Returns the MD5 hash of the message body.
51
+ # @return [String] MD5 hash of the message body
52
+ #
53
+ # @!method body
54
+ # Returns the raw message body as received from SQS.
55
+ # @return [String] The raw message body
56
+ #
57
+ # @!method attributes
58
+ # Returns the SQS message attributes (system attributes).
59
+ # @return [Hash] System attributes like SenderId, SentTimestamp, etc.
60
+ #
61
+ # @!method md5_of_message_attributes
62
+ # Returns the MD5 hash of the message attributes.
63
+ # @return [String] MD5 hash of message attributes
64
+ #
65
+ # @!method message_attributes
66
+ # Returns custom message attributes set by the sender.
67
+ # @return [Hash] Custom message attributes with typed values
7
68
  def_delegators(:data,
8
69
  :message_id,
9
70
  :receipt_handle,
@@ -13,8 +74,24 @@ module Shoryuken
13
74
  :md5_of_message_attributes,
14
75
  :message_attributes)
15
76
 
16
- attr_accessor :client, :queue_url, :queue_name, :data
77
+ # @return [Aws::SQS::Client] The SQS client used for message operations
78
+ attr_accessor :client
17
79
 
80
+ # @return [String] The URL of the SQS queue this message came from
81
+ attr_accessor :queue_url
82
+
83
+ # @return [String] The name of the queue this message came from
84
+ attr_accessor :queue_name
85
+
86
+ # @return [Aws::SQS::Types::Message] The raw SQS message data
87
+ attr_accessor :data
88
+
89
+ # Creates a new Message instance wrapping SQS message data.
90
+ #
91
+ # @param client [Aws::SQS::Client] The SQS client for message operations
92
+ # @param queue [Shoryuken::Queue] The queue this message came from
93
+ # @param data [Aws::SQS::Types::Message] The raw SQS message data
94
+ # @api private
18
95
  def initialize(client, queue, data)
19
96
  self.client = client
20
97
  self.data = data
@@ -22,6 +99,12 @@ module Shoryuken
22
99
  self.queue_name = queue.name
23
100
  end
24
101
 
102
+ # Deletes this message from the SQS queue.
103
+ # Once deleted, the message will not be redelivered and cannot be retrieved again.
104
+ # This is typically called after successful message processing when auto_delete is disabled.
105
+ #
106
+ # @return [Aws::SQS::Types::DeleteMessageResult] The deletion result
107
+ # @raise [Aws::SQS::Errors::ServiceError] If the deletion fails
25
108
  def delete
26
109
  client.delete_message(
27
110
  queue_url: queue_url,
@@ -29,12 +112,42 @@ module Shoryuken
29
112
  )
30
113
  end
31
114
 
115
+ # Changes the visibility timeout of this message with additional options.
116
+ # This allows you to hide the message from other consumers for a longer or shorter period.
117
+ #
118
+ # @param options [Hash] Options to pass to change_message_visibility
119
+ # @option options [Integer] :visibility_timeout New visibility timeout in seconds
120
+ # @return [Aws::SQS::Types::ChangeMessageVisibilityResult] The change result
121
+ # @raise [Aws::SQS::Errors::ServiceError] If the change fails
122
+ #
123
+ # @example Extending visibility with additional options
124
+ # sqs_msg.change_visibility(visibility_timeout: 300)
125
+ #
126
+ # @see #visibility_timeout= For a simpler interface
32
127
  def change_visibility(options)
33
128
  client.change_message_visibility(
34
129
  options.merge(queue_url: queue_url, receipt_handle: data.receipt_handle)
35
130
  )
36
131
  end
37
132
 
133
+ # Sets the visibility timeout for this message.
134
+ # This is a convenience method for changing only the visibility timeout.
135
+ #
136
+ # @param timeout [Integer] New visibility timeout in seconds (0-43200)
137
+ # @return [Aws::SQS::Types::ChangeMessageVisibilityResult] The change result
138
+ # @raise [Aws::SQS::Errors::ServiceError] If the change fails
139
+ #
140
+ # @example Extending processing time
141
+ # def perform(sqs_msg, body)
142
+ # if complex_processing_needed?(body)
143
+ # sqs_msg.visibility_timeout = 1800 # 30 minutes
144
+ # end
145
+ #
146
+ # process_message(body)
147
+ # end
148
+ #
149
+ # @example Making message immediately visible again
150
+ # sqs_msg.visibility_timeout = 0 # Make visible immediately
38
151
  def visibility_timeout=(timeout)
39
152
  client.change_message_visibility(
40
153
  queue_url: queue_url,