job-iteration 1.1.9 → 1.1.14

Sign up to get free protection for your applications and to get access to all the features.
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'