job-iteration 1.1.9 → 1.1.14

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.

Potentially problematic release.


This version of job-iteration might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a4821d55797f7597b3c916d6fbca2195c054bd3aab0ffcc40c9ce21ae0b1ec4
4
- data.tar.gz: e71ce0fb7ec744b502f018ccfc7ea5e819bfe06f9fb2ca5a32c70cc1b875abc7
3
+ metadata.gz: 0da9051861fb27696febf2d0191b4d2f459ece64b34cf0231398984c0f36ef3d
4
+ data.tar.gz: 5fc84c784cdc1a0558a96891a236ac3cea315068e32a6730d633427ef584204b
5
5
  SHA512:
6
- metadata.gz: 943795fabf60cff76fa7b8678f1f5d5a83bd98340194e142092dc891ff1abe991c0410572cebd09155c6c220ff96aa5530fb27e1b4fe119c6bbbf22fe12670ef
7
- data.tar.gz: 11ea6f0165aa31350c6486236a33d66c94316a08f52905aeaab049388b433c229ccf52e69ae3e43eb8bca9a5c56dc4aae46803aae4add9d2a052d5d49af14d8d
6
+ metadata.gz: f30dbb37ada6149fdd877c4b92fff5cf91e078202408230cbdd19593a341c25f4311b4d12dcdceb539a1553e30e0386930a0b49e4c082a4261a0e788c82125b6
7
+ data.tar.gz: 88e9846617f7c45959b45328e46b1465b5acb43caf1a5e0b9fa4a5d0848d00d842392ef4383e372f68c7ccb7aa3f0954549a64846cd3e15c712d9a0d7a60d781
@@ -0,0 +1,47 @@
1
+ name: CI
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ name: Ruby ${{ matrix.ruby }} | Gemfile ${{ matrix.gemfile }}
9
+ services:
10
+ redis:
11
+ image: redis
12
+ ports:
13
+ - 6379:6379
14
+ strategy:
15
+ matrix:
16
+ ruby: [2.5, 2.6, 2.7, 3.0]
17
+ gemfile: [rails_5_2, rails_6_0, rails_edge]
18
+ exclude:
19
+ - ruby: 2.5
20
+ gemfile: rails_edge
21
+ - ruby: 2.6
22
+ gemfile: rails_edge
23
+ - ruby: 3.0
24
+ gemfile: rails_5_2
25
+ env:
26
+ BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
27
+ steps:
28
+ - name: Check out code
29
+ uses: actions/checkout@v2
30
+ - name: Set up Ruby ${{ matrix.ruby }}
31
+ uses: ruby/setup-ruby@v1
32
+ with:
33
+ ruby-version: ${{ matrix.ruby }}
34
+ bundler-cache: true
35
+ - name: Start MySQL and create DB
36
+ run: |
37
+ sudo systemctl start mysql.service
38
+ mysql -uroot -h localhost -proot -e "CREATE DATABASE job_iteration_test;"
39
+ - name: Rubocop
40
+ run: bundle exec rubocop
41
+ - name: Ruby tests
42
+ run: bundle exec rake test
43
+ env:
44
+ REDIS_HOST: localhost
45
+ REDIS_PORT: ${{ job.services.redis.ports[6379] }}
46
+ - name: Documentation correctly written
47
+ run: bundle exec yardoc --no-output --no-save --no-stats --fail-on-warning
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
- inherit_from:
2
- - http://shopify.github.io/ruby-style-guide/rubocop.yml
1
+ inherit_gem:
2
+ rubocop-shopify: rubocop.yml
3
3
 
4
4
  AllCops:
5
5
  TargetRubyVersion: 2.4.4
data/CHANGELOG.md CHANGED
@@ -1,12 +1,38 @@
1
1
  ### Master (unreleased)
2
2
 
3
+ ## v1.1.14 (May 28, 2021)
4
+
5
+ #### Bug fix
6
+ - [84](https://github.com/Shopify/job-iteration/pull/84) - Call adjust_total_time before running on_complete callbacks
7
+ - [94](https://github.com/Shopify/job-iteration/pull/94) - Remove unnecessary break
8
+ - [95](https://github.com/Shopify/job-iteration/pull/95) - ActiveRecordBatchEnumerator#each should rewind at the end
9
+ - [97](https://github.com/Shopify/job-iteration/pull/97) - Batch enumerator size returns the number of batches, not records
10
+
11
+ ## v1.1.13 (May 20, 2021)
12
+
3
13
  #### New feature
14
+ - [91](https://github.com/Shopify/job-iteration/pull/91) - Add enumerator yielding batches as Active Record Relations
15
+
16
+ ## v1.1.12 (April 19, 2021)
17
+
18
+ #### Bug fix
19
+
20
+ - [77](https://github.com/Shopify/job-iteration/pull/77) - Defer enforce cursor be serializable until 2.0.0
21
+
22
+ ## v1.1.11 (April 19, 2021)
4
23
 
5
24
  #### Bug fix
6
25
 
26
+ - [73](https://github.com/Shopify/job-iteration/pull/73) - Enforce cursor be serializable
27
+ _This is reverted in 1.1.12 as it breaks behaviour in some apps._
28
+
29
+ ## v1.1.10 (March 30, 2021)
30
+
31
+ - [69](https://github.com/Shopify/job-iteration/pull/69) - Fix memory leak in ActiveRecordCursor
32
+
7
33
  ## v1.1.9 (January 6, 2021)
8
34
 
9
- - [61](https://github.com/Shopify/job-iteration/pull/61) Call `super` in `method_added`
35
+ - [61](https://github.com/Shopify/job-iteration/pull/61) - Call `super` in `method_added`
10
36
 
11
37
  ## v1.1.8 (June 8, 2020)
12
38
 
data/Gemfile CHANGED
@@ -8,21 +8,22 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
8
8
  gemspec
9
9
 
10
10
  # for integration testing
11
- gem 'sidekiq'
12
- gem 'resque'
11
+ gem "sidekiq"
12
+ gem "resque"
13
13
 
14
- gem 'mysql2', '~> 0.5'
15
- gem 'globalid'
16
- gem 'i18n'
17
- gem 'redis'
18
- gem 'database_cleaner'
14
+ gem "mysql2", "~> 0.5"
15
+ gem "globalid"
16
+ gem "i18n"
17
+ gem "redis"
18
+ gem "database_cleaner"
19
19
 
20
- gem 'pry'
21
- gem 'mocha'
20
+ gem "pry"
21
+ gem "mocha"
22
22
 
23
- gem 'rubocop', '~> 0.77.0'
24
- gem 'yard'
25
- gem 'rake'
23
+ gem "rubocop-shopify", require: false
24
+ gem "rubocop", "<= 1.12.1", require: false # 1.13.0 drops Ruby 2.4 support
25
+ gem "yard"
26
+ gem "rake"
26
27
 
27
28
  # for unit testing optional sorbet support
28
- gem 'sorbet-runtime'
29
+ gem "sorbet-runtime"
data/README.md CHANGED
@@ -77,7 +77,28 @@ class BatchesJob < ApplicationJob
77
77
 
78
78
  def each_iteration(batch_of_comments, product_id)
79
79
  # batch_of_comments will contain batches of 100 records
80
- Comment.where(id: batch_of_comments.map(&:id)).update_all(deleted: true)
80
+ batch_of_comments.each do |comment|
81
+ DeleteCommentJob.perform_later(comment)
82
+ end
83
+ end
84
+ end
85
+ ```
86
+
87
+ ```ruby
88
+ class BatchesAsRelationJob < ApplicationJob
89
+ include JobIteration::Iteration
90
+
91
+ def build_enumerator(product_id, cursor:)
92
+ enumerator_builder.active_record_on_batch_relations(
93
+ Product.find(product_id).comments,
94
+ cursor: cursor,
95
+ batch_size: 100,
96
+ )
97
+ end
98
+
99
+ def each_iteration(batch_of_comments, product_id)
100
+ # batch_of_comments will be a Comment::ActiveRecord_Relation
101
+ batch_of_comments.update_all(deleted: true)
81
102
  end
82
103
  end
83
104
  ```
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- eval_gemfile '../Gemfile'
3
+ eval_gemfile "../Gemfile"
4
4
 
5
- gem 'activejob', '~> 5.2.0'
6
- gem 'activerecord', '~> 5.2.0'
5
+ gem "activejob", "~> 5.2.0"
6
+ gem "activerecord", "~> 5.2.0"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- eval_gemfile '../Gemfile'
3
+ eval_gemfile "../Gemfile"
4
4
 
5
- gem 'activejob', '~> 6.0.0'
6
- gem 'activerecord', '~> 6.0.0'
5
+ gem "activejob", "~> 6.0.0"
6
+ gem "activerecord", "~> 6.0.0"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- eval_gemfile '../Gemfile'
3
+ eval_gemfile "../Gemfile"
4
4
 
5
- gem 'activejob', github: 'rails/rails'
6
- gem 'activerecord', github: 'rails/rails'
5
+ gem "activejob", github: "rails/rails", branch: "main"
6
+ gem "activerecord", github: "rails/rails", branch: "main"
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.authors = %w(Shopify)
11
11
  spec.email = ["ops-accounts+shipit@shopify.com"]
12
12
 
13
- spec.summary = 'Makes your background jobs interruptible and resumable.'
13
+ spec.summary = "Makes your background jobs interruptible and resumable."
14
14
  spec.description = spec.summary
15
15
  spec.homepage = "https://github.com/shopify/job-iteration"
16
16
  spec.license = "MIT"
data/lib/job-iteration.rb CHANGED
@@ -54,11 +54,11 @@ module JobIteration
54
54
  def load_integration(integration)
55
55
  unless INTEGRATIONS.include?(integration)
56
56
  raise IntegrationLoadError,
57
- "#{integration} integration is not supported. Available integrations: #{INTEGRATIONS.join(', ')}"
57
+ "#{integration} integration is not supported. Available integrations: #{INTEGRATIONS.join(", ")}"
58
58
  end
59
59
 
60
60
  require_relative "./job-iteration/integrations/#{integration}"
61
61
  end
62
62
  end
63
63
 
64
- JobIteration.load_integrations unless ENV['ITERATION_DISABLE_AUTOCONFIGURE']
64
+ JobIteration.load_integrations unless ENV["ITERATION_DISABLE_AUTOCONFIGURE"]
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobIteration
4
+ # Builds Batch Enumerator based on ActiveRecord Relation.
5
+ # @see EnumeratorBuilder
6
+ class ActiveRecordBatchEnumerator
7
+ include Enumerable
8
+
9
+ SQL_DATETIME_WITH_NSEC = "%Y-%m-%d %H:%M:%S.%N"
10
+
11
+ def initialize(relation, columns: nil, batch_size: 100, cursor: nil)
12
+ @batch_size = batch_size
13
+ @primary_key = "#{relation.table_name}.#{relation.primary_key}"
14
+ @columns = Array(columns&.map(&:to_s) || @primary_key)
15
+ @primary_key_index = @columns.index(@primary_key) || @columns.index(relation.primary_key)
16
+ @pluck_columns = if @primary_key_index
17
+ @columns
18
+ else
19
+ @columns.dup << @primary_key
20
+ end
21
+ @cursor = Array.wrap(cursor)
22
+ @initial_cursor = @cursor
23
+ raise ArgumentError, "Must specify at least one column" if @columns.empty?
24
+ if relation.joins_values.present? && !@columns.all? { |column| column.to_s.include?(".") }
25
+ raise ArgumentError, "You need to specify fully-qualified columns if you join a table"
26
+ end
27
+
28
+ if relation.arel.orders.present? || relation.arel.taken.present?
29
+ raise ConditionNotSupportedError
30
+ end
31
+
32
+ @base_relation = relation.reorder(@columns.join(","))
33
+ end
34
+
35
+ def each
36
+ return to_enum { size } unless block_given?
37
+ while (relation = next_batch)
38
+ yield relation, cursor_value
39
+ end
40
+ end
41
+
42
+ def size
43
+ (@base_relation.count + @batch_size - 1) / @batch_size # ceiling division
44
+ end
45
+
46
+ private
47
+
48
+ def next_batch
49
+ relation = @base_relation.limit(@batch_size)
50
+ if conditions.any?
51
+ relation = relation.where(*conditions)
52
+ end
53
+
54
+ cursor_values, ids = relation.uncached do
55
+ pluck_columns(relation)
56
+ end
57
+
58
+ cursor = cursor_values.last
59
+ unless cursor.present?
60
+ @cursor = @initial_cursor
61
+ return
62
+ end
63
+ # The primary key was plucked, but original cursor did not include it, so we should remove it
64
+ cursor.pop unless @primary_key_index
65
+ @cursor = Array.wrap(cursor)
66
+
67
+ # Yields relations by selecting the primary keys of records in the batch.
68
+ # Post.where(published: nil) results in an enumerator of relations like: Post.where(ids: batch_of_ids)
69
+ @base_relation.where(@primary_key => ids)
70
+ end
71
+
72
+ def pluck_columns(relation)
73
+ if @pluck_columns.size == 1 # only the primary key
74
+ column_values = relation.pluck(*@pluck_columns)
75
+ return [column_values, column_values]
76
+ end
77
+
78
+ column_values = relation.pluck(*@pluck_columns)
79
+ primary_key_index = @primary_key_index || -1
80
+ primary_key_values = column_values.map { |values| values[primary_key_index] }
81
+
82
+ serialize_column_values!(column_values)
83
+ [column_values, primary_key_values]
84
+ end
85
+
86
+ def cursor_value
87
+ return @cursor.first if @cursor.size == 1
88
+ @cursor
89
+ end
90
+
91
+ def conditions
92
+ column_index = @cursor.size - 1
93
+ column = @columns[column_index]
94
+ where_clause = if @columns.size == @cursor.size
95
+ "#{column} > ?"
96
+ else
97
+ "#{column} >= ?"
98
+ end
99
+ while column_index > 0
100
+ column_index -= 1
101
+ column = @columns[column_index]
102
+ where_clause = "#{column} > ? OR (#{column} = ? AND (#{where_clause}))"
103
+ end
104
+ ret = @cursor.reduce([where_clause]) { |params, value| params << value << value }
105
+ ret.pop
106
+ ret
107
+ end
108
+
109
+ def serialize_column_values!(column_values)
110
+ column_values.map! { |values| values.map! { |value| column_value(value) } }
111
+ end
112
+
113
+ def column_value(value)
114
+ value.is_a?(Time) ? value.strftime(SQL_DATETIME_WITH_NSEC) : value
115
+ end
116
+ end
117
+ end
@@ -23,7 +23,7 @@ module JobIteration
23
23
  @columns = Array.wrap(columns)
24
24
  self.position = Array.wrap(position)
25
25
  raise ArgumentError, "Must specify at least one column" if columns.empty?
26
- if relation.joins_values.present? && !@columns.all? { |column| column.to_s.include?('.') }
26
+ if relation.joins_values.present? && !@columns.all? { |column| column.to_s.include?(".") }
27
27
  raise ArgumentError, "You need to specify fully-qualified columns if you join a table"
28
28
  end
29
29
 
@@ -31,7 +31,7 @@ module JobIteration
31
31
  raise ConditionNotSupportedError
32
32
  end
33
33
 
34
- @base_relation = relation.reorder(@columns.join(','))
34
+ @base_relation = relation.reorder(@columns.join(","))
35
35
  @reached_end = false
36
36
  end
37
37
 
@@ -50,7 +50,7 @@ module JobIteration
50
50
 
51
51
  def update_from_record(record)
52
52
  self.position = @columns.map do |column|
53
- method = column.to_s.split('.').last
53
+ method = column.to_s.split(".").last
54
54
  record.send(method.to_sym)
55
55
  end
56
56
  end
@@ -64,7 +64,9 @@ module JobIteration
64
64
  relation = relation.where(*conditions)
65
65
  end
66
66
 
67
- records = relation.to_a
67
+ records = relation.uncached do
68
+ relation.to_a
69
+ end
68
70
 
69
71
  update_from_record(records.last) unless records.empty?
70
72
  @reached_end = records.size < batch_size
@@ -40,7 +40,7 @@ module JobIteration
40
40
 
41
41
  def cursor_value(record)
42
42
  positions = @columns.map do |column|
43
- attribute_name = column.to_s.split('.').last
43
+ attribute_name = column.to_s.split(".").last
44
44
  column_value(record, attribute_name)
45
45
  end
46
46
  return positions.first if positions.size == 1
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require_relative "./active_record_batch_enumerator"
2
3
  require_relative "./active_record_enumerator"
3
4
  require_relative "./csv_enumerator"
4
5
  require_relative "./throttle_enumerator"
@@ -86,6 +87,11 @@ module JobIteration
86
87
  # WHERE (created_at > '$LAST_CREATED_AT_CURSOR'
87
88
  # OR (created_at = '$LAST_CREATED_AT_CURSOR' AND (id > '$LAST_ID_CURSOR')))
88
89
  # ORDER BY created_at, id LIMIT 100
90
+ #
91
+ # As a result of this query pattern, if the values in these columns change for the records in scope during
92
+ # iteration, they may be skipped or yielded multiple times depending on the nature of the update and the
93
+ # cursor's value. If the value gets updated to a greater value than the cursor's value, it will get yielded
94
+ # again. Similarly, if the value gets updated to a lesser value than the curor's value, it will get skipped.
89
95
  def build_active_record_enumerator_on_records(scope, cursor:, **args)
90
96
  enum = build_active_record_enumerator(
91
97
  scope,
@@ -95,7 +101,7 @@ module JobIteration
95
101
  wrap(self, enum)
96
102
  end
97
103
 
98
- # Builds Enumerator from Active Record Relation and enumerates on batches.
104
+ # Builds Enumerator from Active Record Relation and enumerates on batches of records.
99
105
  # Each Enumerator tick moves the cursor +batch_size+ rows forward.
100
106
  #
101
107
  # +batch_size:+ sets how many records will be fetched in one batch. Defaults to 100.
@@ -110,6 +116,16 @@ module JobIteration
110
116
  wrap(self, enum)
111
117
  end
112
118
 
119
+ # Builds Enumerator from Active Record Relation and enumerates on batches, yielding Active Record Relations.
120
+ # See documentation for #build_active_record_enumerator_on_batches.
121
+ def build_active_record_enumerator_on_batch_relations(scope, cursor:, **args)
122
+ JobIteration::ActiveRecordBatchEnumerator.new(
123
+ scope,
124
+ cursor: cursor,
125
+ **args
126
+ ).each
127
+ end
128
+
113
129
  def build_throttle_enumerator(enum, throttle_on:, backoff:)
114
130
  JobIteration::ThrottleEnumerator.new(
115
131
  enum,
@@ -124,6 +140,7 @@ module JobIteration
124
140
  alias_method :array, :build_array_enumerator
125
141
  alias_method :active_record_on_records, :build_active_record_enumerator_on_records
126
142
  alias_method :active_record_on_batches, :build_active_record_enumerator_on_batches
143
+ alias_method :active_record_on_batch_relations, :build_active_record_enumerator_on_batch_relations
127
144
  alias_method :throttle, :build_throttle_enumerator
128
145
 
129
146
  private
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'resque'
3
+ require "resque"
4
4
 
5
5
  module JobIteration
6
6
  module Integrations
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'sidekiq'
3
+ require "sidekiq"
4
4
 
5
5
  module JobIteration
6
6
  module Integrations # @private
@@ -1,11 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/all'
3
+ require "active_support/all"
4
4
 
5
5
  module JobIteration
6
6
  module Iteration
7
7
  extend ActiveSupport::Concern
8
8
 
9
+ class CursorError < ArgumentError
10
+ attr_reader :cursor
11
+
12
+ def initialize(message, cursor:)
13
+ super(message)
14
+ @cursor = cursor
15
+ end
16
+
17
+ def message
18
+ "#{super} (#{inspected_cursor})"
19
+ end
20
+
21
+ private
22
+
23
+ def inspected_cursor
24
+ cursor.inspect
25
+ rescue NoMethodError
26
+ # For those brave enough to try to use BasicObject as cursor. Nice try.
27
+ Object.instance_method(:inspect).bind(cursor).call
28
+ end
29
+ end
30
+
9
31
  included do |_base|
10
32
  attr_accessor(
11
33
  :cursor_position,
@@ -54,17 +76,17 @@ module JobIteration
54
76
 
55
77
  def serialize # @private
56
78
  super.merge(
57
- 'cursor_position' => cursor_position,
58
- 'times_interrupted' => times_interrupted,
59
- 'total_time' => total_time,
79
+ "cursor_position" => cursor_position,
80
+ "times_interrupted" => times_interrupted,
81
+ "total_time" => total_time,
60
82
  )
61
83
  end
62
84
 
63
85
  def deserialize(job_data) # @private
64
86
  super
65
- self.cursor_position = job_data['cursor_position']
66
- self.times_interrupted = job_data['times_interrupted'] || 0
67
- self.total_time = job_data['total_time'] || 0
87
+ self.cursor_position = job_data["cursor_position"]
88
+ self.times_interrupted = job_data["times_interrupted"] || 0
89
+ self.total_time = job_data["total_time"] || 0
68
90
  end
69
91
 
70
92
  def perform(*params) # @private
@@ -120,6 +142,9 @@ module JobIteration
120
142
  arguments = arguments.dup.freeze
121
143
  found_record = false
122
144
  enumerator.each do |object_from_enumerator, index|
145
+ # Deferred until 2.0.0
146
+ # assert_valid_cursor!(index)
147
+
123
148
  record_unit_of_work do
124
149
  found_record = true
125
150
  each_iteration(object_from_enumerator, *arguments)
@@ -137,6 +162,8 @@ module JobIteration
137
162
  "times_interrupted=#{times_interrupted} cursor_position=#{cursor_position}"
138
163
  ) unless found_record
139
164
 
165
+ adjust_total_time
166
+
140
167
  true
141
168
  end
142
169
 
@@ -176,6 +203,18 @@ module JobIteration
176
203
  EOS
177
204
  end
178
205
 
206
+ # The adapter must be able to serialize and deserialize the cursor back into an equivalent object.
207
+ # https://github.com/mperham/sidekiq/wiki/Best-Practices#1-make-your-job-parameters-small-and-simple
208
+ def assert_valid_cursor!(cursor)
209
+ return if serializable?(cursor)
210
+
211
+ raise CursorError.new(
212
+ "Cursor must be composed of objects capable of built-in (de)serialization: " \
213
+ "Strings, Integers, Floats, Arrays, Hashes, true, false, or nil.",
214
+ cursor: cursor,
215
+ )
216
+ end
217
+
179
218
  def assert_implements_methods!
180
219
  unless respond_to?(:each_iteration, true)
181
220
  raise(
@@ -212,8 +251,6 @@ module JobIteration
212
251
  end
213
252
 
214
253
  def output_interrupt_summary
215
- adjust_total_time
216
-
217
254
  message = "[JobIteration::Iteration] Completed iterating. times_interrupted=%d total_time=%.3f"
218
255
  logger.info(Kernel.format(message, times_interrupted, total_time))
219
256
  end
@@ -251,5 +288,21 @@ module JobIteration
251
288
  end
252
289
  false
253
290
  end
291
+
292
+ SIMPLE_SERIALIZABLE_CLASSES = [String, Integer, Float, NilClass, TrueClass, FalseClass].freeze
293
+ private_constant :SIMPLE_SERIALIZABLE_CLASSES
294
+ def serializable?(object)
295
+ # Subclasses must be excluded, hence not using is_a? or ===.
296
+ if object.instance_of?(Array)
297
+ object.all? { |element| serializable?(element) }
298
+ elsif object.instance_of?(Hash)
299
+ object.all? { |key, value| serializable?(key) && serializable?(value) }
300
+ else
301
+ SIMPLE_SERIALIZABLE_CLASSES.any? { |klass| object.instance_of?(klass) }
302
+ end
303
+ rescue NoMethodError
304
+ # BasicObject doesn't respond to instance_of, but we can't serialize it anyway
305
+ false
306
+ end
254
307
  end
255
308
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JobIteration
4
- VERSION = "1.1.9"
4
+ VERSION = "1.1.14"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: job-iteration
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.9
4
+ version: 1.1.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-01-06 00:00:00.000000000 Z
11
+ date: 2021-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -45,9 +45,9 @@ executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
+ - ".github/workflows/ci.yml"
48
49
  - ".gitignore"
49
50
  - ".rubocop.yml"
50
- - ".travis.yml"
51
51
  - ".yardopts"
52
52
  - CHANGELOG.md
53
53
  - CODE_OF_CONDUCT.md
@@ -66,6 +66,7 @@ files:
66
66
  - guides/throttling.md
67
67
  - job-iteration.gemspec
68
68
  - lib/job-iteration.rb
69
+ - lib/job-iteration/active_record_batch_enumerator.rb
69
70
  - lib/job-iteration/active_record_cursor.rb
70
71
  - lib/job-iteration/active_record_enumerator.rb
71
72
  - lib/job-iteration/csv_enumerator.rb
@@ -98,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
99
  - !ruby/object:Gem::Version
99
100
  version: '0'
100
101
  requirements: []
101
- rubygems_version: 3.0.3
102
+ rubygems_version: 3.2.17
102
103
  signing_key:
103
104
  specification_version: 4
104
105
  summary: Makes your background jobs interruptible and resumable.
data/.travis.yml DELETED
@@ -1,19 +0,0 @@
1
- services:
2
- - mysql
3
- - redis-server
4
- language: ruby
5
- rvm:
6
- - 2.5
7
- - 2.6
8
- - 2.7
9
- before_install:
10
- - mysql -e 'CREATE DATABASE job_iteration_test;'
11
- script:
12
- - bundle exec rake test
13
- - bundle exec rubocop
14
- - bundle exec yardoc --no-output --no-save --no-stats --fail-on-warning
15
-
16
- gemfile:
17
- - 'gemfiles/rails_5_2.gemfile'
18
- - 'gemfiles/rails_6_0.gemfile'
19
- - 'gemfiles/rails_edge.gemfile'