super_spreader 0.1.0.beta2 → 0.1.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.
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "super_spreader/peak_schedule"
4
- require "super_spreader/redis_model"
5
-
6
- module SuperSpreader
7
- class SchedulerConfig < RedisModel
8
- # The job class to enqueue on each run of the scheduler.
9
- attribute :job_class_name, :string
10
- # The number of records to process in each invocation of the job class.
11
- attribute :batch_size, :integer
12
- # The amount of work to enqueue, in seconds.
13
- attribute :duration, :integer
14
-
15
- # The number of jobs to enqueue per second, allowing for fractional amounts
16
- # such as 1 job every other second using `0.5`.
17
- attribute :per_second_on_peak, :float
18
- # The same as per_second_on_peak, but for times that are not identified as
19
- # on-peak.
20
- attribute :per_second_off_peak, :float
21
-
22
- # This section manages the definition "on peak." Compare this terminology
23
- # to bus or train schedules.
24
-
25
- # The timezone to use for time calculations.
26
- #
27
- # Example: "America/Los_Angeles" for Pacific time
28
- attribute :on_peak_timezone, :string
29
- # The 24-hour hour on which on-peak application usage starts.
30
- #
31
- # Example: 5 for 5 AM
32
- attribute :on_peak_hour_begin, :integer
33
- # The 24-hour hour on which on-peak application usage ends.
34
- #
35
- # Example: 17 for 5 PM
36
- attribute :on_peak_hour_end, :integer
37
- # The wday value on which on-peak application usage starts.
38
- #
39
- # Example: 1 for Monday
40
- attribute :on_peak_wday_begin, :integer
41
- # The wday value on which on-peak application usage ends.
42
- #
43
- # Example: 5 for Friday
44
- attribute :on_peak_wday_end, :integer
45
-
46
- attr_writer :schedule
47
-
48
- def job_class
49
- job_class_name.constantize
50
- end
51
-
52
- def super_spreader_config
53
- [job_class, job_class.super_spreader_model_class]
54
- end
55
-
56
- def spread_options
57
- {
58
- batch_size: batch_size,
59
- duration: duration,
60
- per_second: per_second
61
- }
62
- end
63
-
64
- def per_second
65
- schedule.on_peak? ? per_second_on_peak : per_second_off_peak
66
- end
67
-
68
- private
69
-
70
- def schedule
71
- @schedule ||=
72
- PeakSchedule.new(
73
- on_peak_wday_range: on_peak_wday_begin..on_peak_wday_end,
74
- on_peak_hour_range: on_peak_hour_begin..on_peak_hour_end,
75
- timezone: on_peak_timezone
76
- )
77
- end
78
- end
79
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_job"
4
- require "json"
5
- require "super_spreader/scheduler_config"
6
- require "super_spreader/spreader"
7
- require "super_spreader/stop_signal"
8
-
9
- module SuperSpreader
10
- class SchedulerJob < ActiveJob::Base
11
- extend StopSignal
12
-
13
- def perform
14
- return if self.class.stopped?
15
-
16
- log(started_at: Time.current.iso8601)
17
- log(config.serializable_hash)
18
-
19
- super_spreader = Spreader.new(*config.super_spreader_config)
20
- next_id = super_spreader.enqueue_spread(**config.spread_options)
21
- log(next_id: next_id)
22
-
23
- return if next_id.zero?
24
-
25
- self.class.set(wait_until: next_run_at).perform_later
26
- log(next_run_at: next_run_at.iso8601)
27
- end
28
-
29
- def next_run_at
30
- config.duration.seconds.from_now
31
- end
32
-
33
- def config
34
- @config ||= SchedulerConfig.new
35
- end
36
-
37
- private
38
-
39
- def log(hash)
40
- SuperSpreader.logger.info({subject: self.class.name}.merge(hash).to_json)
41
- end
42
- end
43
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_record"
4
- require "redis"
5
-
6
- module SuperSpreader
7
- class SpreadTracker
8
- def initialize(job_class, model_class)
9
- @job_class = job_class
10
- @model_class = model_class
11
- end
12
-
13
- def initial_id
14
- redis_value = redis.hget(initial_id_key, @model_class.name)
15
-
16
- value = redis_value || @model_class.maximum(:id)
17
-
18
- value.to_i
19
- end
20
-
21
- def initial_id=(value)
22
- if value.nil?
23
- redis.hdel(initial_id_key, @model_class.name)
24
- else
25
- redis.hset(initial_id_key, @model_class.name, value)
26
- end
27
- end
28
-
29
- private
30
-
31
- def redis
32
- SuperSpreader.redis
33
- end
34
-
35
- def initial_id_key
36
- "#{@job_class.name}:initial_id"
37
- end
38
- end
39
- end
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "super_spreader/spread_tracker"
4
-
5
- module SuperSpreader
6
- class Spreader
7
- def initialize(job_class, model_class, spread_tracker: nil)
8
- @job_class = job_class
9
- @model_class = model_class
10
- @spread_tracker = spread_tracker || SpreadTracker.new(job_class, model_class)
11
- end
12
-
13
- def spread(batch_size:, duration:, per_second:, initial_id:, begin_at: Time.now.utc)
14
- end_id = initial_id
15
- segment_duration = 1.0 / per_second
16
- time_index = 0.0
17
- batches = []
18
-
19
- while time_index < duration
20
- break if end_id <= 0
21
-
22
- # Use floor to prevent subsecond times
23
- run_at = begin_at + time_index.floor
24
- begin_id = clamp(end_id - batch_size + 1)
25
- batches << {run_at: run_at, begin_id: begin_id, end_id: end_id}
26
-
27
- break if begin_id == 1
28
-
29
- end_id = begin_id - 1
30
- time_index += segment_duration
31
- end
32
-
33
- batches
34
- end
35
-
36
- def enqueue_spread(**opts)
37
- initial_id = @spread_tracker.initial_id
38
- return 0 if initial_id.zero?
39
-
40
- batches = spread(**opts.merge(initial_id: initial_id))
41
-
42
- batches.each do |batch|
43
- @job_class
44
- .set(wait_until: batch[:run_at])
45
- .perform_later(batch[:begin_id], batch[:end_id])
46
- end
47
-
48
- last_begin_id = batches.last[:begin_id]
49
- next_id = last_begin_id - 1
50
- @spread_tracker.initial_id = next_id
51
-
52
- next_id
53
- end
54
-
55
- private
56
-
57
- def clamp(value)
58
- (value <= 0) ? 1 : value
59
- end
60
- end
61
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "redis"
4
-
5
- module SuperSpreader
6
- module StopSignal
7
- def stop!
8
- redis.set(stop_key, true)
9
- end
10
-
11
- def go!
12
- redis.del(stop_key)
13
- end
14
-
15
- def stopped?
16
- redis.exists(stop_key).positive?
17
- end
18
-
19
- private
20
-
21
- def redis
22
- SuperSpreader.redis
23
- end
24
-
25
- def stop_key
26
- "#{name}:stop"
27
- end
28
- end
29
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SuperSpreader
4
- VERSION = "0.1.0.beta2"
5
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "super_spreader/version"
4
-
5
- require "super_spreader/batch_helper"
6
- require "super_spreader/peak_schedule"
7
- require "super_spreader/redis_model"
8
- require "super_spreader/scheduler_config"
9
- require "super_spreader/scheduler_job"
10
- require "super_spreader/spread_tracker"
11
- require "super_spreader/spreader"
12
- require "super_spreader/stop_signal"
13
-
14
- module SuperSpreader
15
- class Error < StandardError; end
16
-
17
- class << self
18
- attr_accessor :logger, :redis
19
- end
20
- end
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- lib = File.expand_path("../lib", __FILE__)
4
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require "super_spreader/version"
6
-
7
- Gem::Specification.new do |spec|
8
- spec.name = "super_spreader"
9
- spec.version = SuperSpreader::VERSION
10
- spec.authors = ["Benjamin Oakes"]
11
- spec.email = ["boakes@doximity.com"]
12
-
13
- spec.summary = "ActiveJob-based backfill orchestration library"
14
- spec.description = "Provides tools for managing resource-efficient backfills of large datasets via ActiveJob"
15
- spec.homepage = "https://github.com/doximity/super_spreader"
16
-
17
- spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = spec.homepage
19
- spec.metadata["changelog_uri"] = "https://github.com/doximity/super_spreader/blob/main/CHANGELOG.md"
20
-
21
- # Specify which files should be added to the gem when it is released.
22
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
- spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
24
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
- end
26
- spec.executables = []
27
- spec.require_paths = ["lib"]
28
-
29
- spec.add_dependency "activejob", ">= 6.1", "< 8.0"
30
- spec.add_dependency "activemodel", ">= 6.1", "< 8.0"
31
- spec.add_dependency "activerecord", ">= 6.1", "< 8.0"
32
- spec.add_dependency "activesupport", ">= 6.1", "< 8.0"
33
- spec.add_dependency "redis", ">= 4.8", "< 6.0"
34
- spec.add_development_dependency "bundler"
35
- spec.add_development_dependency "factory_bot"
36
- spec.add_development_dependency "guard"
37
- spec.add_development_dependency "guard-rspec"
38
- spec.add_development_dependency "pry"
39
- spec.add_development_dependency "rake"
40
- spec.add_development_dependency "rspec"
41
- spec.add_development_dependency "rspec-rails"
42
- spec.add_development_dependency "rspec_junit_formatter"
43
- spec.add_development_dependency "sqlite3"
44
- spec.add_development_dependency "standard"
45
- spec.add_development_dependency "yard"
46
- end