super_spreader 0.1.0.beta2 → 0.1.0

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