sidekiq_portal 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4786c06610a30caf5b841f54ae358f2d8e03ddbfa0a4455e67d6bc54525881ee
4
+ data.tar.gz: 7b85cfb3ccfd8f4bf334a7c3b79203bccc12d043e570565e76d8c6be3da43ebb
5
+ SHA512:
6
+ metadata.gz: 132e849038540986890f5d240beac2bf82141633825b5d5dd44a52f26e0f524b8727c9f939e321889134fbc4b6f8042a7183e01380eec5c324e3344aa5d950f9
7
+ data.tar.gz: 847dd5b9699d0c64ed686a7e8fa1cc151a94488357a8b345883185e23d0343d26cbf6f798eafbd36b20e3e9731a1d1cec72934dd339f7b101a0bf0d2ca30e277
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .rspec_status
10
+ Gemfile.lock
11
+ /.idea
12
+ .ruby-version
13
+ /.vscode/
14
+ /spec/artifacts/
15
+ /gemfiles/*.gemfile.lock
16
+ *.gem
@@ -0,0 +1 @@
1
+ debug.fullTrace=true
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format progress
3
+ --require spec_helper
@@ -0,0 +1,24 @@
1
+ inherit_gem:
2
+ armitage-rubocop:
3
+ - lib/rubocop.general.yml
4
+ - lib/rubocop.rake.yml
5
+ - lib/rubocop.rspec.yml
6
+
7
+ AllCops:
8
+ TargetRubyVersion: 2.6.5
9
+ Include:
10
+ - lib/**/*.rb
11
+ - spec/**/*.rb
12
+ - Gemfile
13
+ - Rakefile
14
+ - sidekiq_portal.gemspec
15
+ - bin/console
16
+ - bin/rspec
17
+
18
+ # NOTE: support for old ruby versions
19
+ Style/RedundantBegin:
20
+ Enabled: false
21
+
22
+ # NOTE: too situative
23
+ Metrics/ParameterLists:
24
+ Enabled: false
@@ -0,0 +1,16 @@
1
+ sudo: false
2
+ language: ruby
3
+ before_install: gem install bundler
4
+ script: bundle exec rspec
5
+ cache: bundler
6
+ matrix:
7
+ fast_finish: true
8
+ include:
9
+ - rvm: 2.4.9
10
+ - rvm: 2.5.7
11
+ - rvm: 2.6.5
12
+ - rvm: ruby-head
13
+ - rvm: jruby-head
14
+ allow_failures:
15
+ - rvm: ruby-head
16
+ - rvm: jruby-head
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ ## [0.1.0] - 2019-12-24
5
+
6
+ - Release 😈
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at iamdaiver@icloud.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
5
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Rustam Ibragimov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,163 @@
1
+ # Sidekiq::Portal [![Gem Version](https://badge.fury.io/rb/sidekiq_portal.svg)](https://badge.fury.io/rb/sidekiq_portal) [![Build Status](https://travis-ci.org/0exp/sidekiq_portal.svg?branch=master)](https://travis-ci.org/0exp/sidekiq_portal)
2
+
3
+ **Sidekiq::Portal** - scheduled jobs runner for your test environments,
4
+ which execution process must occur during the `Timecop.travel(...)` operations according to the scheduler config.
5
+
6
+ Each job starts at the time it was supposed to start according to the scheduler plan -
7
+ the internal `Time.current` expression will give you exactly the scheduler-planned time.
8
+
9
+ Supports **ActiveJob** backend (**Sidekiq::Worker** coming soon). Works with **sidekiq-scheduler**-based job configs (**sidekiq-cron** coming soon).
10
+
11
+ Realized as an instance, but have a global-based implementation too.
12
+
13
+ ## Table of contents
14
+
15
+ - [Installation](#installation)
16
+ - [Configuration](#configuration)
17
+ - [Example](#example)
18
+ - [Roadmap](#roadmap)
19
+ - [License](#license)
20
+ - [Authors](#authors)
21
+
22
+ ## Installation
23
+
24
+ - in your `Gemfile`
25
+
26
+ ```ruby
27
+ gem 'sidekiq', '>= 5' # runtime dependency
28
+ gem 'timecop', '~> 0.9' # runtime dependency
29
+
30
+ group :test do
31
+ gem 'rspec'
32
+ gem 'sidekiq-portal'
33
+ end
34
+ ```
35
+
36
+ - run shell command:
37
+
38
+ ```shell
39
+ bundle install
40
+ ```
41
+
42
+ - `scec_helper.rb`:
43
+
44
+ ```ruby
45
+ require 'timecop' # runtime dependency
46
+ require 'sidekiq' # runtime dependency
47
+ require 'sidekiq/api'
48
+ require 'sidekiq/testing'
49
+
50
+ require 'sidekiq_portal'
51
+ ```
52
+
53
+ ## Configuration
54
+
55
+ - `default_timezone` (`UTC` by default) - global time zone for your jobs;
56
+ - `retries_count` - recurring job simulation;
57
+ - `scheduler_config` - `sidekiq-scheduler`-based scheduler configuration;;
58
+ - `Sidekiq::Portal.reload!(&configuration)` - reload portal configurations;
59
+
60
+ In your `spec_helper.rb`:
61
+
62
+ ```ruby
63
+ # portal configuration
64
+ Sidekiq::Portal.setup! do |config|
65
+ config.default_timezone = 'UTC'
66
+ config.retries_count = 1
67
+
68
+ # pre-defined sidekiq-scheduler configs (Rails example)
69
+ config.scheduler_cofnig = Rails.application.config_for(:sidekiq)[:schedule]
70
+
71
+ # manual sidekiq-scheduler configs
72
+ config.schedluer_config = {
73
+ LoolJob: { every: '15m' },
74
+ kek_job: { cron: '0 * * * * *', class: :KekJob }
75
+ }
76
+ end
77
+
78
+ # global state clearing logic
79
+ RSpec.configure do |config|
80
+ config.before { Sidekiq::Worker.clear_all }
81
+ config.after { Timecop.return }
82
+ config.after { Sidekiq::Portal.reload! }
83
+ end
84
+ ```
85
+
86
+ And in your tests:
87
+
88
+ ```ruby
89
+ RSpec.describe 'Some spec' do
90
+ specify 'magic?' do
91
+ Timecop.travel(Time.current + 2.hours) # magic begins here 😈
92
+ end
93
+ end
94
+ ```
95
+
96
+ ## Example
97
+
98
+ - Job class:
99
+
100
+ ```ruby
101
+ class HookExampleJob < ApplicationJob
102
+ def perform
103
+ GLOBAL_HOOK_INTERCEPTOR << Time.current # intercept current time
104
+ end
105
+ end
106
+ ```
107
+
108
+ - `Sidekiq::Scheduler` config:
109
+
110
+ ```yaml
111
+ :schedule:
112
+ HookExample:
113
+ every: '15m'
114
+ ```
115
+
116
+ - `HookExampleJob` spec:
117
+
118
+ ```ruby
119
+ RSpec.describe 'HookExampleJob sheduler plan' do
120
+ specify 'scheduled?' do
121
+ stub_const('GLOBAL_HOOK_INTERCEPTOR', [])
122
+ expect(GLOBAL_HOOK_INTERCEPTOR.count).to eq(0) # => true
123
+
124
+ # do some magic 😈
125
+ Timecop.travel(Time.current + 2.hours)
126
+
127
+ expect(GLOBAL_HOOK_INTERCEPTOR.count).to eq(8) # => true (😈 magic)
128
+
129
+ puts GLOBAL_HOOK_INTERCEPTOR # 😈
130
+ # => outputs:
131
+ # 2019-12-24 03:05:39 +0300 (+15m) (Time.current from HookExampleJob#perform)
132
+ # 2019-12-24 03:20:39 +0300 (+15m)
133
+ # 2019-12-24 03:35:39 +0300 (+15m)
134
+ # 2019-12-24 03:50:39 +0300 (+15m)
135
+ # 2019-12-24 04:05:39 +0300 (+15m)
136
+ # 2019-12-24 04:20:39 +0300 (+15m)
137
+ # 2019-12-24 04:35:39 +0300 (+15m)
138
+ # 2019-12-24 04:50:39 +0300 (+15m)
139
+ end
140
+ end
141
+ ```
142
+
143
+ ## Roadmap
144
+
145
+ - `Sidekiq::Testing.portal!` test mode with support for `:inline` and `:fake`;
146
+ (`Sidekiq::Testing.inline!` and `Sidekiq::Testing.fake` respectively);
147
+ - support for `ActiveSupport::Timezone` instances in `default_timezone` config;
148
+ - support for retries;
149
+ - rspec matchers;
150
+ - `#reload!` should use previosly defined settings?;
151
+ - support for `Sidekiq::Worker` job backend;
152
+ - support for `Sidekiq::Cron` scheduler plans;
153
+ - more specs;
154
+ - documentation and examples for instance-based portals (`Sidekiq::Portal.new(&configuration)`);
155
+ - job execution randomization (for jobs which should be invoked at the same time);
156
+
157
+ ## License
158
+
159
+ Released under MIT License.
160
+
161
+ ## Authors
162
+
163
+ [Rustam Ibragimov](https://github.com/0exp)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop'
6
+ require 'rubocop/rake_task'
7
+ require 'rubocop-performance'
8
+ require 'rubocop-rspec'
9
+ require 'rubocop-rake'
10
+
11
+ RuboCop::RakeTask.new(:rubocop) do |t|
12
+ config_path = File.expand_path(File.join('.rubocop.yml'), __dir__)
13
+ t.options = ['--config', config_path]
14
+ t.requires << 'rubocop-rspec'
15
+ t.requires << 'rubocop-performance'
16
+ t.requires << 'rubocop-rake'
17
+ end
18
+
19
+ RSpec::Core::RakeTask.new(:rspec)
20
+
21
+ task default: :rspec
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'sidekiq_portal'
6
+
7
+ require 'pry'
8
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api public
4
+ # @since 0.1.0
5
+ class Sidekiq::Portal::Config < Qonfig::DataSet
6
+ # @return [String]
7
+ #
8
+ # @api private
9
+ # @since 0.1.0
10
+ DEFAULT_TIMEZONE = 'UTC'
11
+
12
+ # @return [Hash]
13
+ #
14
+ # @api private
15
+ # @since 0.1.0
16
+ EMPTY_SCHEDULER_CONFIG = {}.freeze
17
+
18
+ # @return [Integer]
19
+ #
20
+ # @api private
21
+ # @since 0.1.0
22
+ DEFAUL_RETRIES_COUNT = 0
23
+
24
+ setting :default_timezone, DEFAULT_TIMEZONE
25
+ setting :retries_count, DEFAUL_RETRIES_COUNT
26
+ setting :scheduler_config, EMPTY_SCHEDULER_CONFIG
27
+
28
+ validate :default_timezone do |value|
29
+ value.is_a?(String) && !ActiveSupport::TimeZone[value].nil?
30
+ end
31
+
32
+ validate :retries_count, :integer, strict: true
33
+ validate :scheduler_config, :hash, strict: true
34
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ module Sidekiq::Portal::CoreExtensions
6
+ require_relative 'core_extensions/timecop'
7
+ require_relative 'core_extensions/sidekiq_worker'
8
+ require_relative 'core_extensions/sidekiq_queue'
9
+
10
+ # @since 0.1.0
11
+ @extensions_load_lock = Mutex.new
12
+ # @since 0.1.0
13
+ @extensions_loaded = false
14
+
15
+ class << self
16
+ # @return [void]
17
+ #
18
+ # @api private
19
+ # @since 0.1.0
20
+ def load!
21
+ @extensions_load_lock.synchronize do
22
+ unless @extensions_loaded
23
+ check_dependencies!
24
+ load_timecop_extension!
25
+ load_sidekiq_extension!
26
+ @extensions_loaded = true
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # @return [void]
34
+ #
35
+ # @raise [Sidekiq::Portal::CoreDependencyNotFoundError]
36
+ # @raise [Sidekiq::Portal::UnsupportedCoreDependencyError]
37
+ #
38
+ # @api private
39
+ # @since 0.1.0
40
+ def check_dependencies!
41
+ unless defined?(::Timecop)
42
+ raise(Sidekiq::Portal::CoreDependencyNotFoundError, '::Timecop not found')
43
+ end
44
+
45
+ unless defined?(::Sidekiq)
46
+ raise(Sidekiq::Portal::CoreDependencyNotFoundError, 'Sidekiq not found')
47
+ end
48
+
49
+ unless Gem::Version.new(::Timecop::VERSION) >= Gem::Version.new('0.9')
50
+ raise(Sidekiq::Portal::UnsupportedCoreDependencyError, 'Supports timecop@>=0.9 only')
51
+ end
52
+
53
+ unless Gem::Version.new(::Sidekiq::VERSION) >= Gem::Version.new('5')
54
+ raise(Sidekiq::Portal::UnsupportedCoreDependencyError, 'Supports sidekiq@>=5 only')
55
+ end
56
+
57
+ unless defined?(::Sidekiq::Queue)
58
+ raise(Sidekiq::Portal::CoreDependencyNotFoundError, 'Sidekiq::Queue not found')
59
+ end
60
+
61
+ unless defined?(::Sidekiq::Worker)
62
+ raise(Sidekiq::Portal::CoreDependencyNotFoundError, 'Sidekiq::Worker not found')
63
+ end
64
+
65
+ unless defined?(::Sidekiq::Testing)
66
+ raise(Sidekiq::Portal::CoreDependencyNotFoundError, 'Sidekiq::Testing not found')
67
+ end
68
+ end
69
+
70
+ # @return [void]
71
+ #
72
+ # @api private
73
+ # @since 0.1.0
74
+ def load_timecop_extension!
75
+ ::Timecop.prepend(Sidekiq::Portal::CoreExtensions::Timecop)
76
+ end
77
+
78
+ # @return [void]
79
+ #
80
+ # @api private
81
+ # @since 0.1.0
82
+ def load_sidekiq_extension!
83
+ ::Sidekiq::Queue.singleton_class.prepend(Sidekiq::Portal::CoreExtensions::SidekiqQueue)
84
+ ::Sidekiq::Worker.singleton_class.prepend(Sidekiq::Portal::CoreExtensions::SidekiqWorker)
85
+ end
86
+ end
87
+ end