├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── app
├── controllers
│ └── circuit_switch
│ │ ├── application_controller.rb
│ │ └── circuit_switch_controller.rb
└── views
│ ├── circuit_switch
│ └── circuit_switch
│ │ ├── edit.html.erb
│ │ └── index.html.erb
│ └── layouts
│ └── circuit_switch
│ └── application.html.erb
├── bin
├── console
└── setup
├── circuit_switch.gemspec
├── circuit_switch.png
├── config
└── routes.rb
├── lib
├── circuit_switch.rb
├── circuit_switch
│ ├── builder.rb
│ ├── configuration.rb
│ ├── core.rb
│ ├── engine.rb
│ ├── notification.rb
│ ├── orm
│ │ └── active_record
│ │ │ └── circuit_switch.rb
│ ├── railtie.rb
│ ├── stacktrace_modifier.rb
│ ├── tasks.rb
│ ├── tasks
│ │ └── circuit_switch.rake
│ ├── version.rb
│ └── workers
│ │ ├── due_date_notifier.rb
│ │ ├── reporter.rb
│ │ └── run_count_updater.rb
└── generators
│ └── circuit_switch
│ ├── install_generator.rb
│ ├── migration_generator.rb
│ └── templates
│ ├── add_key.rb.erb
│ ├── initializer.rb
│ ├── make_key_unique.rb.erb
│ └── migration.rb.erb
└── test
├── circuit_switch_test.rb
├── lib
├── builder_test.rb
├── core_test.rb
├── orm
│ └── active_record
│ │ └── circuit_switch_test.rb
└── workers
│ ├── due_date_notifier_test.rb
│ ├── reporter_test.rb
│ └── run_count_updater_test.rb
├── support
└── setup_database.rb
└── test_helper.rb
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | strategy:
8 | fail-fast: false
9 | matrix:
10 | os: [ubuntu-latest, windows-latest, macos-latest]
11 | ruby: [2.6, 2.7, 3.0, 3.1, 3.2, 3.3, head]
12 | include:
13 | - { os: macos-13, ruby: '2.3' }
14 | - { os: macos-13, ruby: '2.4' }
15 | - { os: macos-13, ruby: '2.5' }
16 | runs-on: ${{ matrix.os }}
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Set up Ruby
20 | uses: ruby/setup-ruby@v1
21 | with:
22 | ruby-version: ${{ matrix.ruby }}
23 | bundler-cache: true
24 | - name: Run the default task
25 | run: |
26 | bundle exec rake
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 | Gemfile.lock
10 | .byebug_history
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.5.1
2 |
3 | * Remove warning related `CircuitSwitch.open?` without `close_if_reach_limit` option.
4 |
5 | ## 0.5.0
6 |
7 | ### New features
8 |
9 | * GUI has been released!
10 | If you are on Rails, add the following to your `config/routes.rb` and access `/circuit_switch`.
11 |
12 | ```ruby
13 | Rails.application.routes.draw do
14 | mount CircuitSwitch::Engine => 'circuit_switch'
15 | end
16 | ```
17 |
18 | ## 0.4.1
19 |
20 | * Fix bug `if` `stop_report_if_reach_limit` options don't receive `false`.
21 | * Fix to report error with stacktrace.
22 | * Fix not to update run count when run isn't executable.
23 |
24 | ## 0.4.0
25 |
26 | ### Breaking Changes
27 |
28 | * Be able to choice to notify `CircuitSwitch::CalledNotification` or `String`.
29 | Improve `config/initializers/circuit_switch.rb` like following.
30 |
31 | ```diff
32 | CircuitSwitch.configure do |config|
33 | - config.reporter = ->(message) { Bugsnag.notify(message) }
34 | + config.reporter = ->(message, error) { Bugsnag.notify(error) }
35 | ```
36 |
37 | ## 0.3.0
38 |
39 | ### Breaking Changes
40 |
41 | * Modify `key` to unique by default.
42 | To migrate, run next.
43 |
44 | ```
45 | rails generate circuit_switch:migration circuit_switch make_key_unique
46 | rails db:migrate
47 | ```
48 |
49 | ### Changes
50 |
51 | * Fix to save switch when block for `CircuitSwitch.run` raises error.
52 |
53 | ## 0.2.2
54 |
55 | ### New features
56 |
57 | * Add `key_column_name` to configuration for aliasing `circuit_switches.key`.
58 |
59 | ### Changes
60 |
61 | * Declare dependent on ActiveSupport instead of implicitly dependent.
62 |
63 | ## 0.2.1
64 |
65 | ### New features
66 |
67 | * Add `initially_closed` option to `run` and `open?`.
68 | * Add `key` argument for easy handling for human more than caller.
69 | To migrate, run next.
70 |
71 | ```
72 | rails generate circuit_switch:migration circuit_switch add_key
73 | rails db:migrate
74 | ```
75 |
76 | ### Changes
77 |
78 | * Modify log level from warn to info when default value for `close_if_reach_limit` is used.
79 | * Suppress warning that ivar is not initialized.
80 |
81 | ## 0.2.0
82 |
83 | ### Breaking Changes
84 |
85 | * Modify default value of `CircuitSwitch.run` argument `close_if_reach_limit` from true to false.
86 |
87 | ## 0.1.2
88 |
89 | * Modify `CircuitSwitch.open?` receives same arguments as `CircuitSwitch.run`
90 |
91 | ## 0.1.1
92 |
93 | * Fix bug due_date is not set.
94 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
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 unright@gmail.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 [https://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: https://contributor-covenant.org
74 | [version]: https://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Specify your gem's dependencies in circuit_switch.gemspec
4 | gemspec
5 |
6 | gem "rake", "~> 12.0"
7 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021 makicamel
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # circuit_switch
2 |
3 | circuit_switch is a gem for 'difficult' application; for example, few tests, too many meta-programming codes, low aggregation classes and few deploys.
4 | This switch helps make changes easier and deploy safely.
5 | You can deploy and check with a short code like following if the change is good or not, and when a problem is found, you can undo it without releasing it.
6 |
7 | ```ruby
8 | if CircuitSwitch.report.open?
9 | # new codes
10 | else
11 | # old codes
12 | end
13 | ```
14 |
15 | You can also specify conditions to release testing features.
16 |
17 | ```ruby
18 | CircuitSwitch.run(if: -> { current_user.testing_new_feature? }) do
19 | # testing feature codes
20 | end
21 | ```
22 |
23 | CircuitSwitch depends on ActiveRecord and ActiveJob.
24 |
25 | ## Installation
26 |
27 | Add this line to your application's Gemfile and run `bundle install`:
28 |
29 | ```ruby
30 | gem 'circuit_switch'
31 | ```
32 |
33 | Run generator to create initializer and modify `config/initizlizers/circuit_switch.rb`:
34 |
35 | ```
36 | rails generate circuit_switch:install
37 | ```
38 |
39 | Generate a migration for ActiveRecord.
40 | This table saves named key, circuit caller, called count, limit count and so on.
41 |
42 | ```
43 | rails generate circuit_switch:migration circuit_switch
44 | rails db:migrate
45 | ```
46 |
47 | ## Usage
48 |
49 | ### `run`
50 |
51 | When you want to deploy and undo it if something unexpected happens, use `CircuitSwitch.run`.
52 |
53 | ```ruby
54 | CircuitSwitch.run do
55 | # experimental codes
56 | end
57 | ```
58 |
59 | `run` basically calls the received proc. But when a condition is met, it closes the circuit and does not evaluate the proc.
60 | To switch circuit opening and closing, a set of options can be set. Without options, the circuit is always open.
61 | You can set `close_if_reach_limit: true` so that the circuit is only open for 10 invocations. The constant 10 comes from the table definition we have arbitrarily chosen. In case you need a larger number, specify it in the `limit_count` option in combination with `close_if_reach_limit: true`, or alter default constraint on `circuit_switches.run_limit_count`.
62 |
63 | - `key`: [String] The identifier to find record by. If `key` has not been passed, `circuit_switches.caller` is chosen as an alternative.
64 | - `if`: [Boolean, Proc] Calls proc when the value of `if` is evaluated truthy (default: true)
65 | - `close_if`: [Boolean, Proc] Calls proc when the value of `close_if` is evaluated falsy (default: false)
66 | - `close_if_reach_limit`: [Boolean] Stops calling proc when `circuit_switches.run_count` has reached `circuit_switches.run_limit_count` (default: false)
67 | - `limit_count`: [Integer] Mutates `circuit_switches.run_limit_count` whose value defined in schema is 10 by default. (default: nil)
68 | Can't be set to 0 when `close_if_reach_limit` is true. This option is only relevant when `close_if_reach_limit` is set to true.
69 | - `initially_closed`: [Boolean] Creates switch with terminated mode (default: false)
70 |
71 | To close the circuit at a specific date, code goes like:
72 |
73 | ```ruby
74 | CircuitSwitch.run(close_if: -> { Date.today >= some_day }) do
75 | # testing codes
76 | end
77 | ```
78 |
79 | Or when the code of concern has been called 1000 times:
80 |
81 | ```ruby
82 | CircuitSwitch.run(close_if_reach_limit: true, limit_count: 1_000) do
83 | # testing codes
84 | end
85 | ```
86 |
87 | To run other codes when circuit closes, `run?` is available.
88 |
89 | ```ruby
90 | circuit_open = CircuitSwitch.run { ... }.run?
91 | unless circuit_open
92 | # other codes
93 | end
94 | ```
95 |
96 | `CircuitSwitch.run.run?` has syntax sugar. `open?` doesn't receive proc.
97 |
98 | ```ruby
99 | if CircuitSwitch.open?
100 | # new codes
101 | else
102 | # old codes
103 | end
104 | ```
105 |
106 | ### `report`
107 |
108 | When you just want to report, set your `reporter` to initializer and then call `CircuitSwitch.report`.
109 |
110 | ```ruby
111 | CircuitSwitch.report(if: some_condition)
112 | ```
113 |
114 | `report` just reports which line of code is called. It doesn't receive proc. It's useful for refactoring or removing dead codes.
115 | Same as `run`, a set of options can be set. By default, this method does not send reports more than 10 times. The constant 10 comes from the table definition we have arbitrarily chosen. In case you need a larger number, specify it in the `limit_count` option, or alter default constraint on `circuit_switches.report_limit_count`.
116 |
117 | - `key`: [String] The identifier to find record by. If `key` has not been passed, `circuit_switches.caller` is chosen as an alternative.
118 | - `if`: [Boolean, Proc] Reports when the value of `if` is evaluated truthy (default: true)
119 | - `stop_report_if`: [Boolean, Proc] Reports when the value of `stop_report_if` is evaluated falsy (default: false)
120 | - `stop_report_if_reach_limit`: [Boolean] Stops reporting when `circuit_switches.report_count` has reached `circuit_switches.report_limit_count` (default: true)
121 | - `limit_count`: [Integer] Mutates `circuit_switches.report_limit_count` whose value defined in schema is 10 by default. (default: nil)
122 | Can't be set to 0 when `stop_report_if_reach_limit` is true.
123 |
124 | To know if `report` has already been executed or not, you can get through `reported?`.
125 | Of course you can chain `report` and `run` or `open?`.
126 |
127 | #### `with_backtrace`
128 |
129 | Reporter reports a short message by default like `Watching process is called for 5th. Report until for 10th.`.
130 | When your reporting tool knows about it's caller and backtrace, this is enough (e.g. Bugsnag).
131 | When your reporting tool just reports plain message (e.g. Slack), you can set `with_backtrace` true to initializer. Then report has a long message with backtrace like:
132 |
133 | ```
134 | Watching process is called for 5th. Report until for 10th.
135 | called_path: /app/services/greetings_service:21 block in validate
136 | /app/services/greetings_service:20 validate
137 | /app/services/greetings_service:5 call
138 | /app/controllers/greetings_controller.rb:93 create
139 | ```
140 |
141 | ## Test
142 |
143 | To test, FactoryBot will look like this;
144 |
145 | ```ruby
146 | FactoryBot.define do
147 | factory :circuit_switch, class: 'CircuitSwitch::CircuitSwitch' do
148 | sequence(:key) { |n| "/path/to/file:#{n}" }
149 | sequence(:caller) { |n| "/path/to/file:#{n}" }
150 | due_date { Date.tomorrow }
151 |
152 | trait :initially_closed do
153 | run_is_terminated { true }
154 | end
155 | end
156 | end
157 | ```
158 |
159 | ## Task
160 |
161 | When find a problem and you want to terminate running or reporting right now, execute a task with it's caller.
162 | You can specify either key or caller.
163 |
164 | ```
165 | rake circuit_switch:terminate_to_run[your_key]
166 | rake circuit_switch:terminate_to_report[/app/services/greetings_service:21 block in validate]
167 | ```
168 |
169 | In case of not Rails applications, add following to your Rakefile:
170 |
171 | ```ruby
172 | require 'circuit_switch/tasks'
173 | ```
174 |
175 | ## Reminder for cleaning up codes
176 |
177 | Too many circuits make codes messed up. We want to remove circuits after several days, but already have too many TODO things enough to remember them.
178 | Let's forget it! Set `due_date_notifier` to initializer and then call `CircuitSwitch::DueDateNotifier` job daily. It will notify the list of currently registered switches.
179 | By default, due_date is 10 days after today. To modify, set `due_date` to initializer.
180 |
181 | ## GUI to manage switches
182 |
183 | 
184 |
185 | GUI is now only for Rails.
186 | Add the following to your `config/routes.rb` and access `/circuit_switch`.
187 |
188 | ```ruby
189 | Rails.application.routes.draw do
190 | mount CircuitSwitch::Engine => 'circuit_switch'
191 | end
192 | ```
193 |
194 | ### Authentication
195 |
196 | In production, you may need access protection.
197 | With Devise, code goes like:
198 |
199 | ```ruby
200 | authenticate :user, lambda { |user| user.admin? } do
201 | mount CircuitSwitch::Engine => 'circuit_switch'
202 | end
203 | ```
204 |
205 | ## Contributing
206 |
207 | Bug reports and pull requests are welcome on GitHub at https://github.com/makicamel/circuit_switch. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/makicamel/circuit_switch/blob/master/CODE_OF_CONDUCT.md).
208 |
209 | ## License
210 |
211 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
212 |
213 | ## Code of Conduct
214 |
215 | Everyone interacting in the CircuitSwitch project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/makicamel/circuit_switch/blob/master/CODE_OF_CONDUCT.md).
216 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rake/testtask"
3 |
4 | Rake::TestTask.new(:test) do |t|
5 | t.libs << "test"
6 | t.libs << "lib"
7 | t.test_files = FileList["test/**/*_test.rb"]
8 | end
9 |
10 | task :default => :test
11 |
--------------------------------------------------------------------------------
/app/controllers/circuit_switch/application_controller.rb:
--------------------------------------------------------------------------------
1 | module CircuitSwitch
2 | class ApplicationController < ActionController::Base
3 | protect_from_forgery with: :exception
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/controllers/circuit_switch/circuit_switch_controller.rb:
--------------------------------------------------------------------------------
1 | require_dependency 'circuit_switch/application_controller'
2 |
3 | module CircuitSwitch
4 | class CircuitSwitchController < ::CircuitSwitch::ApplicationController
5 | def index
6 | @circuit_switches = ::CircuitSwitch::CircuitSwitch.all.order(order_by)
7 | end
8 |
9 | def edit
10 | @circuit_switch = ::CircuitSwitch::CircuitSwitch.find(params[:id])
11 | end
12 |
13 | def update
14 | @circuit_switch = ::CircuitSwitch::CircuitSwitch.find(params[:id])
15 | if @circuit_switch.update circuit_switch_params
16 | flash[:success] = "Switch for `#{@circuit_switch.key}` is successfully updated."
17 | redirect_to circuit_switch_index_path
18 | else
19 | render :edit
20 | end
21 | end
22 |
23 | def destroy
24 | @circuit_switch = ::CircuitSwitch::CircuitSwitch.find(params[:id])
25 | @circuit_switch.destroy!
26 | flash[:success] = "Switch for `#{@circuit_switch.key}` is successfully destroyed."
27 | redirect_to circuit_switch_index_path
28 | end
29 |
30 | private
31 |
32 | def order_by
33 | params[:order_by].in?(%w[id due_date]) ? params[:order_by] : 'id'
34 | end
35 |
36 | def circuit_switch_params
37 | params.require(:circuit_switch).permit(:key, :caller, :run_count, :run_limit_count, :run_is_terminated, :report_count, :report_limit_count, :report_is_terminated, :due_date)
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/app/views/circuit_switch/circuit_switch/edit.html.erb:
--------------------------------------------------------------------------------
1 |
Edit switch
2 |
3 |
4 | <% if @circuit_switch.errors.any? %>
5 |
6 |
7 | <% @circuit_switch.errors.full_messages.each do |message| %>
8 | - <%= message %>
9 | <% end %>
10 |
11 |
12 | <% end %>
13 |
14 | <%= form_with(model: @circuit_switch, local: true) do |form| %>
15 |
16 |
17 |
18 | id: <%= @circuit_switch.id %>
19 |
20 | created_at: <%= @circuit_switch.created_at %> /
21 | updated_at: <%= @circuit_switch.updated_at %>
22 |
23 |
24 |
25 | <%= link_to 'Back', circuit_switch_index_path, class: 'btn btn-outline-dark' %>
26 |
27 |
28 |
29 |
30 |
31 | <%= form.label :key, 'key:', class: 'form-label' %>
32 | <%= form.text_area :key, class: 'form-control font-monospace' %>
33 |
34 |
35 | <%= form.label :caller, 'caller:', class: 'form-label' %>
36 | <%= form.text_area :caller, class: 'form-control font-monospace' %>
37 |
38 |
39 |
40 |
41 |
42 | <%= form.label :run_is_terminated, 'run_mode:', class: 'col-sm-2 col-form-label' %>
43 |
44 | <%= form.select :run_is_terminated, { open: false, closed: true }, { selected: @circuit_switch.run_is_terminated }, { class: ['form-select', (@circuit_switch.run_is_terminated ? 'alert-dark' : 'alert-primary')] } %>
45 |
46 |
47 |
48 |
49 | <%= form.label :run_count, 'run_count:', class: 'col-form-label' %>
50 |
51 |
52 |
53 | <%= form.text_field :run_count, class: 'form-control' %>
54 |
55 |
56 |
57 |
58 | <%= form.label :run_limit_count, 'run_limit_count:', class: 'col-form-label' %>
59 |
60 |
61 |
62 | <%= form.text_field :run_limit_count, class: 'form-control' %>
63 |
64 |
65 |
66 | <%= form.label :report_is_terminated, 'report_mode:', class: 'col-sm-2 col-sm-2 col-form-label' %>
67 |
68 | <%= form.select :report_is_terminated, { reporting: false, terminated: true }, { selected: @circuit_switch.report_is_terminated }, { class: ['form-select', (@circuit_switch.report_is_terminated ? 'alert-dark' : 'alert-primary')] } %>
69 |
70 |
71 |
72 |
73 | <%= form.label :report_count, 'report_count:', class: 'col-form-label' %>
74 |
75 |
76 |
77 | <%= form.text_field :report_count, class: 'form-control' %>
78 |
79 |
80 |
81 |
82 | <%= form.label :report_limit_count, 'report_limit_count:', class: 'col-form-label' %>
83 |
84 |
85 |
86 | <%= form.text_field :report_limit_count, class: 'form-control' %>
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | <%= form.label :due_date, 'due_date:', class: 'col-form-label' %>
95 |
96 |
97 |
98 | <%= form.text_field :due_date, class: 'form-control' %>
99 |
100 |
101 |
102 |
103 |
104 | <%= form.submit 'update', class: 'btn btn-dark' %>
105 |
106 |
107 | <% end %>
108 |
109 | <%= button_to 'delete', circuit_switch_path(@circuit_switch), method: :delete, class: 'btn btn-danger btn-destroy' %>
110 |
111 |
112 |
130 |
--------------------------------------------------------------------------------
/app/views/circuit_switch/circuit_switch/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 | circuit_switch
3 |
4 |
5 |
6 | <% if flash[:success].present? %>
7 |
<%= flash[:success] %>
8 | <% end %>
9 |
10 |
11 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
24 | id |
25 | key |
26 | caller |
27 | due_date |
28 | run |
29 | report |
30 | show/edit |
31 | destroy |
32 |
33 |
34 |
35 |
36 | <% @circuit_switches.each do |circuit_switch| %>
37 |
38 | <%= circuit_switch.id %> |
39 | <%= circuit_switch.key %> |
40 | <%= circuit_switch.caller %> |
41 | <%= circuit_switch.due_date %> |
42 |
43 | <% if circuit_switch.run_is_terminated %>
44 |
45 | <% else %>
46 |
47 | <% end %>
48 | |
49 |
50 | <% if circuit_switch.report_is_terminated %>
51 |
52 | <% else %>
53 |
54 | <% end %>
55 | |
56 |
57 | <%= button_to edit_circuit_switch_path(circuit_switch), method: :get, class: 'btn btn-outline-dark' do %>
58 |
59 | <% end %>
60 | |
61 |
62 | <%= button_to circuit_switch_path(circuit_switch), method: :delete, class: 'btn btn-danger btn-destroy' do %>
63 |
64 | <% end %>
65 | |
66 |
67 | <% end %>
68 |
69 |
70 |
71 |
72 |
84 |
--------------------------------------------------------------------------------
/app/views/layouts/circuit_switch/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | circuit_switch
6 | <%= csrf_meta_tags %>
7 | <%= csp_meta_tag %>
8 | <%= javascript_include_tag 'https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js', integrity: 'sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW', crossorigin: 'anonymous' %>
9 | <%= javascript_include_tag 'https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js' %>
10 | <%= javascript_include_tag 'https://kit.fontawesome.com/3e20336786.js', crossorigin: 'anonymous' %>
11 | <%= stylesheet_link_tag 'https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css', media: 'all', integrity: 'sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1', crossorigin: 'anonymous' %>
12 |
13 |
14 | <%= yield %>
15 |
16 |
17 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "bundler/setup"
4 | require "circuit_switch"
5 |
6 | # You can add fixtures and/or initialization code here to make experimenting
7 | # with your gem easier. You can also use a different console, if you like.
8 |
9 | # (If you use this, don't forget to add pry to your Gemfile!)
10 | # require "pry"
11 | # Pry.start
12 |
13 | require "irb"
14 | IRB.start(__FILE__)
15 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
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
9 |
--------------------------------------------------------------------------------
/circuit_switch.gemspec:
--------------------------------------------------------------------------------
1 | require_relative 'lib/circuit_switch/version'
2 |
3 | Gem::Specification.new do |spec|
4 | spec.name = "circuit_switch"
5 | spec.version = CircuitSwitch::VERSION
6 | spec.authors = ["makicamel"]
7 | spec.email = ["unright@gmail.com"]
8 |
9 | spec.summary = 'Circuit switch with report tools'
10 | spec.description = 'circuit_switch is a gem for \'difficult\' application. This switch helps to make changes easier and deploy safely.'
11 | spec.homepage = 'https://github.com/makicamel/circuit_switch'
12 | spec.license = "MIT"
13 | spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14 |
15 | spec.metadata["homepage_uri"] = spec.homepage
16 | spec.metadata["source_code_uri"] = 'https://github.com/makicamel/circuit_switch'
17 | spec.metadata["changelog_uri"] = 'https://github.com/makicamel/circuit_switch/blob/main/CHANGELOG.md'
18 |
19 | # Specify which files should be added to the gem when it is released.
20 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
22 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23 | end
24 | spec.require_paths = ["lib"]
25 |
26 | spec.add_dependency 'activejob'
27 | spec.add_dependency 'activerecord'
28 | spec.add_dependency 'activesupport'
29 | spec.add_development_dependency 'byebug'
30 | spec.add_development_dependency 'sqlite3'
31 | spec.add_development_dependency 'test-unit'
32 | spec.add_development_dependency 'test-unit-rr'
33 | end
34 |
--------------------------------------------------------------------------------
/circuit_switch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/makicamel/circuit_switch/f541f02e545dcc11e9370e406599cf8024b22fea/circuit_switch.png
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | CircuitSwitch::Engine.routes.draw do
2 | resources 'circuit_switch', only: [:index, :edit, :update, :destroy], controller: 'circuit_switch'
3 | root to: 'circuit_switch#index'
4 | end
5 |
--------------------------------------------------------------------------------
/lib/circuit_switch.rb:
--------------------------------------------------------------------------------
1 | require_relative 'circuit_switch/configuration'
2 | require_relative 'circuit_switch/builder'
3 | require_relative 'circuit_switch/orm/active_record/circuit_switch'
4 | require_relative 'circuit_switch/engine' if defined?(Rails)
5 | require_relative 'circuit_switch/railtie' if defined?(Rails::Railtie)
6 | require_relative 'circuit_switch/version'
7 | require_relative 'circuit_switch/workers/due_date_notifier'
8 |
9 | module CircuitSwitch
10 | class << self
11 | def configure
12 | yield config
13 | end
14 |
15 | def config
16 | @config ||= Configuration.new
17 | end
18 |
19 | # @param key [String] Named key to find switch instead of caller
20 | # @param if [Boolean, Proc] Call proc when `if` is truthy (default: true)
21 | # @param close_if [Boolean, Proc] Call proc when `close_if` is falsy (default: false)
22 | # @param close_if_reach_limit [Boolean] Stop calling proc when run count reaches limit (default: false)
23 | # @param limit_count [Integer] Limit count. Use `run_limit_count` default value if it's nil
24 | # Can't be set 0 when `close_if_reach_limit` is true (default: nil)
25 | # @param initially_closed [Boolean] Create switch with terminated mode (default: false)
26 | # @param [Proc] block
27 | def run(key: nil, if: nil, close_if: nil, close_if_reach_limit: nil, limit_count: nil, initially_closed: nil, &block)
28 | arguments = {
29 | key: key,
30 | if: binding.local_variable_get(:if),
31 | close_if: close_if,
32 | close_if_reach_limit: close_if_reach_limit,
33 | limit_count: limit_count,
34 | initially_closed: initially_closed,
35 | }.reject { |_, v| v.nil? }
36 | Builder.new.run(**arguments, &block)
37 | end
38 |
39 | # @param key [String] Named key to find switch instead of caller
40 | # @param if [Boolean, Proc] Report when `if` is truthy (default: true)
41 | # @param stop_report_if [Boolean, Proc] Report when `close_if` is falsy (default: false)
42 | # @param stop_report_if_reach_limit [Boolean] Stop reporting when reported count reaches limit (default: true)
43 | # @param limit_count [Integer] Limit count. Use `report_limit_count` default value if it's nil
44 | # Can't be set 0 when `stop_report_if_reach_limit` is true (default: nil)
45 | def report(key: nil, if: nil, stop_report_if: nil, stop_report_if_reach_limit: nil, limit_count: nil)
46 | if block_given?
47 | raise ArgumentError.new('CircuitSwitch.report doesn\'t receive block. Use CircuitSwitch.run if you want to pass block.')
48 | end
49 |
50 | arguments = {
51 | key: key,
52 | if: binding.local_variable_get(:if),
53 | stop_report_if: stop_report_if,
54 | stop_report_if_reach_limit: stop_report_if_reach_limit,
55 | limit_count: limit_count
56 | }.reject { |_, v| v.nil? }
57 | Builder.new.report(**arguments)
58 | end
59 |
60 | # Syntax sugar for `CircuitSwitch.run`
61 | # @param key [String] Named key to find switch instead of caller
62 | # @param if [Boolean, Proc] `CircuitSwitch.run` is runnable when `if` is truthy (default: true)
63 | # @param close_if [Boolean, Proc] `CircuitSwitch.run` is runnable when `close_if` is falsy (default: false)
64 | # @param close_if_reach_limit [Boolean] `CircuitSwitch.run` is NOT runnable when run count reaches limit (default: true)
65 | # @param limit_count [Integer] Limit count. Use `run_limit_count` default value if it's nil. Can't be set 0 (default: nil)
66 | # @param initially_closed [Boolean] Create switch with terminated mode (default: false)
67 | # @return [Boolean]
68 | def open?(key: nil, if: nil, close_if: nil, close_if_reach_limit: nil, limit_count: nil, initially_closed: nil)
69 | if block_given?
70 | raise ArgumentError.new('CircuitSwitch.open doesn\'t receive block. Use CircuitSwitch.run if you want to pass block.')
71 | end
72 |
73 | arguments = {
74 | key: key,
75 | if: binding.local_variable_get(:if),
76 | close_if: close_if,
77 | close_if_reach_limit: close_if_reach_limit,
78 | limit_count: limit_count,
79 | initially_closed: initially_closed,
80 | }.reject { |_, v| v.nil? }
81 | Builder.new.run(**arguments) {}.run?
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/lib/circuit_switch/builder.rb:
--------------------------------------------------------------------------------
1 | require_relative 'core'
2 |
3 | module CircuitSwitch
4 | class Builder < Core
5 | def initialize
6 | super
7 | @run = false
8 | @reported = false
9 | end
10 |
11 | def assign_runner(
12 | key: nil,
13 | if: true,
14 | close_if: false,
15 | close_if_reach_limit: false,
16 | limit_count: nil,
17 | initially_closed: false
18 | )
19 | @key = key
20 | @run_if = binding.local_variable_get(:if)
21 | @close_if = close_if
22 | @close_if_reach_limit = close_if_reach_limit
23 | @run_limit_count = limit_count
24 | @initially_closed = initially_closed
25 | end
26 |
27 | def assign_reporter(
28 | key: nil,
29 | if: true,
30 | stop_report_if: false,
31 | stop_report_if_reach_limit: true,
32 | limit_count: nil
33 | )
34 | @key = key
35 | @report_if = binding.local_variable_get(:if)
36 | @stop_report_if = stop_report_if
37 | @stop_report_if_reach_limit = stop_report_if_reach_limit
38 | @report_limit_count = limit_count
39 | end
40 |
41 | def run(key: nil, if: nil, close_if: nil, close_if_reach_limit: nil, limit_count: nil, initially_closed: nil, &block)
42 | arguments = {
43 | key: key,
44 | if: binding.local_variable_get(:if),
45 | close_if: close_if,
46 | close_if_reach_limit: close_if_reach_limit,
47 | limit_count: limit_count,
48 | initially_closed: initially_closed,
49 | }.reject { |_, v| v.nil? }
50 | assign_runner(**arguments)
51 | execute_run(&block)
52 | end
53 |
54 | def report(key: nil, if: nil, stop_report_if: nil, stop_report_if_reach_limit: nil, limit_count: nil)
55 | arguments = {
56 | key: key,
57 | if: binding.local_variable_get(:if),
58 | stop_report_if: stop_report_if,
59 | stop_report_if_reach_limit: stop_report_if_reach_limit,
60 | limit_count: limit_count
61 | }.reject { |_, v| v.nil? }
62 | assign_reporter(**arguments)
63 | execute_report
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/circuit_switch/configuration.rb:
--------------------------------------------------------------------------------
1 | module CircuitSwitch
2 | class Configuration
3 | CIRCUIT_SWITCH = 'circuit_switch'.freeze
4 |
5 | attr_accessor :reporter, :due_date_notifier
6 | attr_writer :report_paths, :report_if, :due_date, :with_backtrace, :allowed_backtrace_paths, :strip_paths
7 |
8 | def report_paths
9 | @report_paths ||= [Rails.root]
10 | end
11 |
12 | def silent_paths=(paths)
13 | @silent_paths = paths.append(CIRCUIT_SWITCH).uniq
14 | end
15 |
16 | def silent_paths
17 | @silent_paths ||= [CIRCUIT_SWITCH]
18 | end
19 |
20 | def report_if
21 | @report_if ||= Rails.env.production?
22 | end
23 |
24 | def enable_report?
25 | report_if.respond_to?(:call) ? report_if.call : !!report_if
26 | end
27 |
28 | def key_column_name=(key)
29 | ::CircuitSwitch::CircuitSwitch.alias_attribute :key, key
30 | end
31 |
32 | def due_date
33 | @due_date ||= Date.today + 10
34 | end
35 |
36 | def with_backtrace
37 | @with_backtrace ||= false
38 | end
39 |
40 | def allowed_backtrace_paths
41 | @allowed_backtrace_paths ||= [Dir.pwd]
42 | end
43 |
44 | def strip_paths
45 | @strip_paths ||= [Dir.pwd]
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/circuit_switch/core.rb:
--------------------------------------------------------------------------------
1 | require_relative 'notification'
2 | require_relative 'workers/reporter'
3 | require_relative 'workers/run_count_updater'
4 |
5 | module CircuitSwitch
6 | class Core
7 | delegate :config, to: ::CircuitSwitch
8 | attr_reader :key, :run_if, :close_if, :close_if_reach_limit, :run_limit_count, :initially_closed,
9 | :report_if, :stop_report_if, :stop_report_if_reach_limit, :report_limit_count
10 |
11 | def execute_run(&block)
12 | run_executable = false
13 | if close_if_reach_limit && run_limit_count == 0
14 | raise CircuitSwitchError.new('Can\'t set limit_count to 0 when close_if_reach_limit is true')
15 | end
16 |
17 | return self if evaluate(close_if) || !evaluate(run_if)
18 | return self if close_if_reach_limit && switch.reached_run_limit?(run_limit_count)
19 | return self if switch.run_is_terminated?
20 |
21 | run_executable = true
22 | unless switch.new_record? && initially_closed
23 | yield
24 | @run = true
25 | end
26 | self
27 | ensure
28 | if run_executable
29 | RunCountUpdater.perform_later(
30 | key: key,
31 | limit_count: run_limit_count,
32 | called_path: called_path,
33 | reported: reported?,
34 | initially_closed: initially_closed
35 | )
36 | end
37 | end
38 |
39 | def execute_report
40 | if config.reporter.nil?
41 | raise CircuitSwitchError.new('Set config.reporter.')
42 | end
43 | if config.reporter.arity == 1
44 | Logger.new($stdout).info('config.reporter now receives 2 arguments. Improve your `config/initializers/circuit_switch.rb`.')
45 | end
46 | if stop_report_if_reach_limit && report_limit_count == 0
47 | raise CircuitSwitchError.new('Can\'t set limit_count to 0 when stop_report_if_reach_limit is true')
48 | end
49 | return self unless config.enable_report?
50 | return self if evaluate(stop_report_if) || !evaluate(report_if)
51 | return self if switch.report_is_terminated?
52 | return self if stop_report_if_reach_limit && switch.reached_report_limit?(report_limit_count)
53 |
54 | Reporter.perform_later(
55 | key: key,
56 | limit_count: report_limit_count,
57 | called_path: called_path,
58 | stacktrace: StacktraceModifier.call(backtrace: caller),
59 | run: run?
60 | )
61 | @reported = true
62 | self
63 | end
64 |
65 | # @return [Boolean]
66 | def run?
67 | @run
68 | end
69 |
70 | # @return [Boolean]
71 | def reported?
72 | @reported
73 | end
74 |
75 | private
76 |
77 | def switch
78 | return @switch if defined? @switch
79 |
80 | if key
81 | @switch = CircuitSwitch.find_or_initialize_by(key: key)
82 | else
83 | @switch = CircuitSwitch.find_or_initialize_by(caller: called_path)
84 | end
85 | end
86 |
87 | def called_path
88 | @called_path ||= caller
89 | .reject { |path| /(#{config.silent_paths.join('|')})/.match?(path) }
90 | .detect { |path| /(#{config.report_paths.join('|')})/.match?(path) }
91 | &.sub(/(#{config.strip_paths.join('|')})/, '')
92 | &.gsub(/[`']/, '') ||
93 | "/somewhere/in/library:in #{Date.today}"
94 | end
95 |
96 | def evaluate(boolean_or_proc)
97 | boolean_or_proc.respond_to?(:call) ? boolean_or_proc.call : boolean_or_proc
98 | end
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/lib/circuit_switch/engine.rb:
--------------------------------------------------------------------------------
1 | module CircuitSwitch
2 | def self.table_name_prefix
3 | ''
4 | end
5 |
6 | class Engine < ::Rails::Engine
7 | isolate_namespace ::CircuitSwitch
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/circuit_switch/notification.rb:
--------------------------------------------------------------------------------
1 | require_relative 'stacktrace_modifier'
2 |
3 | module CircuitSwitch
4 | class CircuitSwitchError < RuntimeError
5 | end
6 |
7 | class CircuitSwitchNotification < RuntimeError
8 | end
9 |
10 | class CalledNotification < CircuitSwitchNotification
11 | def to_message(called_path:)
12 | if ::CircuitSwitch.config.with_backtrace
13 | "#{message}\ncalled_path: #{called_path}\n#{backtrace.join("\n")}"
14 | else
15 | message
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/circuit_switch/orm/active_record/circuit_switch.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 |
3 | module CircuitSwitch
4 | class CircuitSwitch < ::ActiveRecord::Base
5 | validates :key, uniqueness: true
6 |
7 | after_initialize do |switch|
8 | switch.key ||= switch.caller
9 | end
10 |
11 | def assign(run_limit_count: nil, report_limit_count: nil)
12 | self.run_limit_count = run_limit_count if run_limit_count
13 | self.report_limit_count = report_limit_count if report_limit_count
14 | self
15 | end
16 |
17 | def reached_run_limit?(new_value)
18 | if new_value
19 | run_count >= new_value
20 | else
21 | run_count >= run_limit_count
22 | end
23 | end
24 |
25 | def reached_report_limit?(new_value)
26 | if new_value
27 | report_count >= new_value
28 | else
29 | report_count >= report_limit_count
30 | end
31 | end
32 |
33 | def increment_run_count!
34 | with_writable { update!(run_count: run_count + 1) }
35 | end
36 |
37 | def increment_report_count!
38 | with_writable { update!(report_count: report_count + 1) }
39 | end
40 |
41 | def message
42 | process = key == caller ? 'Watching process' : "Process for '#{key}'"
43 | "#{process} is called for #{report_count}th. Report until for #{report_limit_count}th."
44 | end
45 |
46 | private
47 |
48 | def with_writable
49 | if self.class.const_defined?(:ApplicationRecord) && ApplicationRecord.respond_to?(:with_writable)
50 | ApplicationRecord.with_writable { yield }
51 | else
52 | yield
53 | end
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/lib/circuit_switch/railtie.rb:
--------------------------------------------------------------------------------
1 | module CircuitSwitch
2 | class Railtie < ::Rails::Railtie
3 | rake_tasks do
4 | load 'circuit_switch/tasks/circuit_switch.rake'
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/circuit_switch/stacktrace_modifier.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/module/delegation'
2 |
3 | module CircuitSwitch
4 | class StacktraceModifier
5 | class << self
6 | delegate :config, to: ::CircuitSwitch
7 |
8 | def call(backtrace:)
9 | if config.with_backtrace
10 | backtrace
11 | .select { |path| /(#{config.allowed_backtrace_paths.join('|')})/.match?(path) }
12 | .map { |path| path.sub(/(#{config.strip_paths.join('|')})/, '') }
13 | else
14 | backtrace
15 | .select { |path| /(#{config.allowed_backtrace_paths.join('|')})/.match?(path) }
16 | end
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/circuit_switch/tasks.rb:
--------------------------------------------------------------------------------
1 | Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].each do |task|
2 | load task
3 | end
4 |
--------------------------------------------------------------------------------
/lib/circuit_switch/tasks/circuit_switch.rake:
--------------------------------------------------------------------------------
1 | namespace :circuit_switch do
2 | desc 'Update run_is_terminated to true to close circuit'
3 | task :terminate_to_run, ['key_or_caller'] => :environment do |_, arg|
4 | key_or_caller = arg[:key_or_caller]
5 | puts "Start to update run_is_terminated of circuit_switch for '#{key_or_caller}' to true."
6 | sleep(3)
7 |
8 | switch = CircuitSwitch::CircuitSwitch.find_by(key: key_or_caller) || CircuitSwitch::CircuitSwitch.find_by!(caller: key_or_caller)
9 | puts "circuit_switch is found. id: #{switch.id}."
10 |
11 | switch.update(run_is_terminated: true)
12 | puts "Updated run_is_terminated of circuit_switch for '#{key_or_caller}' to true."
13 | end
14 |
15 | desc 'Update report_is_terminated to true to stop reporting'
16 | task :terminate_to_report, ['key_or_caller'] => :environment do |_, arg|
17 | key_or_caller = arg[:key_or_caller]
18 | puts "Start to update report_is_terminated of circuit_switch for '#{key_or_caller}' to true."
19 | sleep(3)
20 |
21 | switch = CircuitSwitch::CircuitSwitch.find_by(key: key_or_caller) || CircuitSwitch::CircuitSwitch.find_by!(caller: key_or_caller)
22 | puts "circuit_switch is found. id: #{switch.id}."
23 |
24 | switch.update(report_is_terminated: true)
25 | puts "Updated report_is_terminated of circuit_switch for '#{key_or_caller}' to true."
26 | end
27 |
28 | desc 'Delete switch'
29 | task :delete_switch, ['key_or_caller'] => :environment do |_, arg|
30 | key_or_caller = arg[:key_or_caller]
31 | puts "Start to delete circuit_switch for '#{key_or_caller}'."
32 | sleep(3)
33 |
34 | switch = CircuitSwitch::CircuitSwitch.find_by(key: key_or_caller) || CircuitSwitch::CircuitSwitch.find_by!(caller: key_or_caller)
35 | puts "circuit_switch is found. id: #{switch.id}."
36 |
37 | switch.destroy!
38 | puts "Successfully deleted circuit_switch for '#{key_or_caller}'."
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/circuit_switch/version.rb:
--------------------------------------------------------------------------------
1 | module CircuitSwitch
2 | VERSION = '0.5.1'
3 | end
4 |
--------------------------------------------------------------------------------
/lib/circuit_switch/workers/due_date_notifier.rb:
--------------------------------------------------------------------------------
1 | require 'date'
2 |
3 | module CircuitSwitch
4 | class DueDateNotifier < ::ActiveJob::Base
5 | delegate :config, to: ::CircuitSwitch
6 |
7 | def perform
8 | raise CircuitSwitchError.new('Set config.due_date_notifier.') unless config.due_date_notifier
9 |
10 | circuit_switches = CircuitSwitch.where('due_date <= ?', Date.today).order(id: :asc)
11 | if circuit_switches.present?
12 | message = "Due date has come! Let's consider about removing switches and cleaning up code! :)\n" +
13 | circuit_switches.map { |switch| "id: #{switch.id}, key: '#{switch.key}', caller: '#{switch.caller}', created_at: #{switch.created_at}" }.join("\n")
14 | config.due_date_notifier.call(message)
15 | else
16 | switches_count = CircuitSwitch.all.size
17 | message = switches_count.zero? ? 'There is no switch!' : "#{switches_count} switches are waiting for their due_date."
18 | config.due_date_notifier.call(message)
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/circuit_switch/workers/reporter.rb:
--------------------------------------------------------------------------------
1 | require 'active_job'
2 | require 'active_support/core_ext/module/delegation'
3 |
4 | module CircuitSwitch
5 | class Reporter < ::ActiveJob::Base
6 | delegate :config, to: ::CircuitSwitch
7 |
8 | def perform(key:, limit_count:, called_path:, stacktrace:, run:)
9 | # Wait for RunCountUpdater saves circuit_switch
10 | sleep(3) if run
11 |
12 | first_raise = true
13 | begin
14 | circuit_switch = key ? CircuitSwitch.find_by(key: key) : CircuitSwitch.find_by(caller: called_path)
15 | if run && circuit_switch.nil?
16 | raise ActiveRecord::RecordNotFound.new('Couldn\'t find CircuitSwitch::CircuitSwitch')
17 | end
18 |
19 | circuit_switch ||= CircuitSwitch.new(key: key, caller: called_path)
20 | circuit_switch.due_date ||= config.due_date
21 | circuit_switch.assign(report_limit_count: limit_count).increment_report_count!
22 | raise CalledNotification.new(circuit_switch.message)
23 | rescue ActiveRecord::RecordInvalid => e
24 | raise e unless first_raise
25 |
26 | first_raise = false
27 | sleep(2)
28 | retry
29 | rescue CalledNotification => notification
30 | notification.set_backtrace(stacktrace)
31 | if config.reporter.arity == 1
32 | config.reporter.call(notification.to_message(called_path: called_path))
33 | else
34 | config.reporter.call(notification.to_message(called_path: called_path), notification)
35 | end
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/circuit_switch/workers/run_count_updater.rb:
--------------------------------------------------------------------------------
1 | require 'active_job'
2 | require 'active_support/core_ext/module/delegation'
3 |
4 | module CircuitSwitch
5 | class RunCountUpdater < ::ActiveJob::Base
6 | delegate :config, to: ::CircuitSwitch
7 |
8 | def perform(key:, limit_count:, called_path:, reported:, initially_closed:)
9 | # Wait for Reporter saves circuit_switch
10 | sleep(3) if reported
11 |
12 | first_raise = true
13 | begin
14 | circuit_switch = key ? CircuitSwitch.find_by(key: key) : CircuitSwitch.find_by(caller: called_path)
15 | if reported && circuit_switch.nil?
16 | raise ActiveRecord::RecordNotFound.new('Couldn\'t find CircuitSwitch::CircuitSwitch')
17 | end
18 |
19 | circuit_switch ||= CircuitSwitch.new(key: key, caller: called_path, run_is_terminated: initially_closed)
20 | circuit_switch.due_date ||= config.due_date
21 | circuit_switch.assign(run_limit_count: limit_count).increment_run_count!
22 | rescue ActiveRecord::RecordInvalid => e
23 | raise e unless first_raise
24 |
25 | first_raise = false
26 | sleep(2)
27 | retry
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/generators/circuit_switch/install_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module CircuitSwitch
4 | class InstallGenerator < ::Rails::Generators::Base
5 | include ::Rails::Generators::Migration
6 | source_root File.expand_path('templates', __dir__)
7 | desc 'Installs CircuitSwitch.'
8 |
9 | def install
10 | template 'initializer.rb', 'config/initializers/circuit_switch.rb'
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/generators/circuit_switch/migration_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails/generators/active_record'
2 |
3 | module CircuitSwitch
4 | class MigrationGenerator < ActiveRecord::Generators::Base
5 | desc 'Create a migration to manage circuit switches state'
6 | source_root File.expand_path('templates', __dir__)
7 | argument :migration_type, required: false, type: :array, default: ['create'],
8 | desc: 'Type of migration to create or add key column or make key unique. By default to create.',
9 | banner: 'create or add_key'
10 |
11 | def generate_migration
12 | case migration_type
13 | when ['add_key']
14 | migration_template 'add_key.rb.erb', 'db/migrate/add_key_to_circuit_switches.rb', migration_version: migration_version
15 | when ['make_key_unique']
16 | migration_template 'make_key_unique.rb.erb', 'db/migrate/make_key_unique_for_circuit_switches.rb', migration_version: migration_version
17 | else
18 | migration_template 'migration.rb.erb', 'db/migrate/create_circuit_switches.rb', migration_version: migration_version
19 | end
20 | end
21 |
22 | def migration_version
23 | if ActiveRecord::VERSION::MAJOR >= 5
24 | "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/generators/circuit_switch/templates/add_key.rb.erb:
--------------------------------------------------------------------------------
1 | class AddKeyToCircuitSwitches < ActiveRecord::Migration<%= migration_version %>
2 | def up
3 | add_column :circuit_switches, :key, :string, after: :id
4 | CircuitSwitch::CircuitSwitch.all.each { |switch| switch.update_column(:key, switch.caller) }
5 | change_column_null :circuit_switches, :key, false
6 | add_index :circuit_switches, :key
7 | end
8 |
9 | def down
10 | remove_index :circuit_switches, :key
11 | remove_column :circuit_switches, :key
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/generators/circuit_switch/templates/initializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | CircuitSwitch.configure do |config|
4 | # Specify proc to call your report tool: like;
5 | # config.reporter = -> (message, error) { Bugsnag.notify(error) }
6 | # config.reporter = -> (message, error) { Sentry::Rails.capture_message(message) }
7 | config.reporter = nil
8 |
9 | # Condition to report
10 | # config.report_if = Rails.env.production?
11 |
12 | # Allowed paths to report
13 | # CircuitSwitch recognizes logic as unique that first match with these paths.
14 | # Allowed all paths when set `[]`.
15 | # config.report_paths = [Rails.root]
16 |
17 | # Excluded paths to report
18 | # config.silent_paths = [CIRCUIT_SWITCH]
19 |
20 | # Alias column name for circuit_switches.key through alias_attribute
21 | # config.key_column_name = :key
22 |
23 | # Notifier to notify circuit_switch's due_date come and it's time to clean code!
24 | # Specify proc to call your report tool: like;
25 | # config.due_date_notifier = -> (message) { Slack::Web::Client.new.chat_postMessage(channel: '#your_channel', text: message) }
26 | # config.due_date_notifier = nil
27 |
28 | # Date for due_date_notifier
29 | # config.due_date = Date.today + 10
30 |
31 | # Option to contain error backtrace for report
32 | # You don't need backtrace when you report to some bug report tool.
33 | # You may want backtrace when reporting to a plain feed; e.g. Slack or email.
34 | # config.with_backtrace = false
35 |
36 | # Allowed backtrace paths to report
37 | # Allowed all paths when set `[]`.
38 | # config.allowed_backtrace_paths = [Dir.pwd]
39 |
40 | # Omit path prefix in caller and backtrace for readability
41 | # config.strip_paths = [Dir.pwd]
42 | end
43 |
--------------------------------------------------------------------------------
/lib/generators/circuit_switch/templates/make_key_unique.rb.erb:
--------------------------------------------------------------------------------
1 | class MakeKeyUniqueForCircuitSwitches < ActiveRecord::Migration<%= migration_version %>
2 | def up
3 | remove_index :circuit_switches, :key
4 | add_index :circuit_switches, :key, unique: true
5 | end
6 |
7 | def down
8 | remove_index :circuit_switches, :key
9 | add_index :circuit_switches, :key
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/generators/circuit_switch/templates/migration.rb.erb:
--------------------------------------------------------------------------------
1 | class CreateCircuitSwitches < ActiveRecord::Migration<%= migration_version %>
2 | def change
3 | create_table :circuit_switches do |t|
4 | t.string :key, null: false
5 | t.string :caller, null: false
6 | t.integer :run_count, default: 0, null: false
7 | t.integer :run_limit_count, default: 10, null: false
8 | t.boolean :run_is_terminated, default: false, null: false
9 | t.integer :report_count, default: 0, null: false
10 | t.integer :report_limit_count, default: 10, null: false
11 | t.boolean :report_is_terminated, default: false, null: false
12 | t.date :due_date, null: false
13 | t.timestamps
14 | end
15 |
16 | add_index :circuit_switches, [:key], unique: true
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/circuit_switch_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class CircuitSwitchTest < Test::Unit::TestCase
4 | def test_run_returns_builder_instance
5 | assert_instance_of(
6 | CircuitSwitch::Builder,
7 | CircuitSwitch.run {}
8 | )
9 | end
10 |
11 | def test_run_assigns_value_when_receives_value
12 | if_value = [true, false].sample
13 | close_if_value = [true, false].sample
14 | close_if_reach_limit_value = [true, false].sample
15 | initially_closed_value = [true, false].sample
16 | builder = CircuitSwitch.run(if: if_value, close_if: close_if_value, close_if_reach_limit: close_if_reach_limit_value, initially_closed: initially_closed_value) {}
17 |
18 | assert_equal(
19 | if_value,
20 | builder.instance_variable_get(:@run_if)
21 | )
22 | assert_equal(
23 | close_if_value,
24 | builder.instance_variable_get(:@close_if)
25 | )
26 | assert_equal(
27 | close_if_reach_limit_value,
28 | builder.instance_variable_get(:@close_if_reach_limit)
29 | )
30 | assert_equal(
31 | initially_closed_value,
32 | builder.instance_variable_get(:@initially_closed)
33 | )
34 | end
35 |
36 | def test_run_assigns_default_value_when_doesnt_receive_value
37 | builder = CircuitSwitch.run {}
38 |
39 | assert_equal(
40 | true,
41 | builder.instance_variable_get(:@run_if)
42 | )
43 | assert_equal(
44 | false,
45 | builder.instance_variable_get(:@close_if)
46 | )
47 | assert_equal(
48 | false,
49 | builder.instance_variable_get(:@close_if_reach_limit)
50 | )
51 | assert_equal(
52 | false,
53 | builder.instance_variable_get(:@initially_closed)
54 | )
55 | end
56 |
57 | def test_report_returns_builder_instance
58 | assert_instance_of(
59 | CircuitSwitch::Builder,
60 | CircuitSwitch.report
61 | )
62 | end
63 |
64 | def test_report_raises_error_if_block_given
65 | assert_raise ArgumentError do
66 | CircuitSwitch.report {}
67 | end
68 | end
69 |
70 | def test_report_assigns_value_when_receives_value
71 | if_value = [true, false].sample
72 | stop_report_if_value = [true, false].sample
73 | stop_report_if_reach_limit_value = [true, false].sample
74 | builder = CircuitSwitch.report(if: if_value, stop_report_if: stop_report_if_value, stop_report_if_reach_limit: stop_report_if_reach_limit_value)
75 |
76 | assert_equal(
77 | if_value,
78 | builder.instance_variable_get(:@report_if)
79 | )
80 | assert_equal(
81 | stop_report_if_value,
82 | builder.instance_variable_get(:@stop_report_if)
83 | )
84 | assert_equal(
85 | stop_report_if_reach_limit_value,
86 | builder.instance_variable_get(:@stop_report_if_reach_limit)
87 | )
88 | end
89 |
90 | def test_report_assigns_default_value_when_doesnt_receive_value
91 | builder = CircuitSwitch.report
92 |
93 | assert_equal(
94 | true,
95 | builder.instance_variable_get(:@report_if)
96 | )
97 | assert_equal(
98 | false,
99 | builder.instance_variable_get(:@stop_report_if)
100 | )
101 | assert_equal(
102 | true,
103 | builder.instance_variable_get(:@stop_report_if_reach_limit)
104 | )
105 | end
106 |
107 | def test_open_returns_boolean
108 | assert_equal(
109 | true,
110 | CircuitSwitch.open?
111 | )
112 | end
113 |
114 | def test_open_raises_error_if_block_given
115 | assert_raise ArgumentError do
116 | CircuitSwitch.open? {}
117 | end
118 | end
119 |
120 | def test_open_returns_same_value_with_run_run_when_receives_value
121 | if_value = [true, false].sample
122 | close_if_value = [true, false].sample
123 | close_if_reach_limit_value = [true, false].sample
124 | initially_closed_value = [true, false].sample
125 |
126 | assert_equal(
127 | CircuitSwitch.run(if: if_value, close_if: close_if_value, close_if_reach_limit: close_if_reach_limit_value, initially_closed: initially_closed_value) {}.run?,
128 | CircuitSwitch.open?(if: if_value, close_if: close_if_value, close_if_reach_limit: close_if_reach_limit_value, initially_closed: initially_closed_value)
129 | )
130 | end
131 |
132 | def test_open_returns_same_value_with_run_run_when_doesnt_receive_value
133 | assert_equal(
134 | CircuitSwitch.run {}.run?,
135 | CircuitSwitch.open?
136 | )
137 | end
138 | end
139 |
--------------------------------------------------------------------------------
/test/lib/builder_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class BuilderTest < Test::Unit::TestCase
4 | include ::CircuitSwitch::TestHelper
5 |
6 | def test_run_assigns_value_when_receives_value
7 | builder = CircuitSwitch::Builder.new
8 | if_value = [true, false].sample
9 | close_if_value = [true, false].sample
10 | close_if_reach_limit_value = [true, false].sample
11 | initially_closed_value = [true, false].sample
12 | builder.run(if: if_value, close_if: close_if_value, close_if_reach_limit: close_if_reach_limit_value, initially_closed: initially_closed_value) {}
13 |
14 | assert_equal(
15 | if_value,
16 | builder.instance_variable_get(:@run_if)
17 | )
18 | assert_equal(
19 | close_if_value,
20 | builder.instance_variable_get(:@close_if)
21 | )
22 | assert_equal(
23 | close_if_reach_limit_value,
24 | builder.instance_variable_get(:@close_if_reach_limit)
25 | )
26 | assert_equal(
27 | initially_closed_value,
28 | builder.instance_variable_get(:@initially_closed)
29 | )
30 | end
31 |
32 | def test_run_assigns_default_value_when_doesnt_receive_value
33 | builder = CircuitSwitch::Builder.new
34 | builder.run {}
35 |
36 | assert_equal(
37 | true,
38 | builder.instance_variable_get(:@run_if)
39 | )
40 | assert_equal(
41 | false,
42 | builder.instance_variable_get(:@close_if)
43 | )
44 | assert_equal(
45 | false,
46 | builder.instance_variable_get(:@close_if_reach_limit)
47 | )
48 | assert_equal(
49 | false,
50 | builder.instance_variable_get(:@initially_closed)
51 | )
52 | end
53 |
54 | def test_report_assigns_value_when_receives_value
55 | builder = CircuitSwitch::Builder.new
56 | if_value = [true, false].sample
57 | stop_report_if_value = [true, false].sample
58 | stop_report_if_reach_limit_value = [true, false].sample
59 | builder.report(if: if_value, stop_report_if: stop_report_if_value, stop_report_if_reach_limit: stop_report_if_reach_limit_value)
60 |
61 | assert_equal(
62 | if_value,
63 | builder.instance_variable_get(:@report_if)
64 | )
65 | assert_equal(
66 | stop_report_if_value,
67 | builder.instance_variable_get(:@stop_report_if)
68 | )
69 | assert_equal(
70 | stop_report_if_reach_limit_value,
71 | builder.instance_variable_get(:@stop_report_if_reach_limit)
72 | )
73 | end
74 |
75 | def test_report_assigns_default_value_when_doesnt_receive_value
76 | builder = CircuitSwitch::Builder.new
77 | builder.report
78 |
79 | assert_equal(
80 | true,
81 | builder.instance_variable_get(:@report_if)
82 | )
83 | assert_equal(
84 | false,
85 | builder.instance_variable_get(:@stop_report_if)
86 | )
87 | assert_equal(
88 | true,
89 | builder.instance_variable_get(:@stop_report_if_reach_limit)
90 | )
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/test/lib/core_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class CoreTest < Test::Unit::TestCase
4 | include ::CircuitSwitch::TestHelper
5 |
6 | setup do
7 | CircuitSwitch::CircuitSwitch.truncate
8 | end
9 |
10 | def runner(arguments = {})
11 | core = CircuitSwitch::Builder.new
12 | core.assign_runner(**arguments)
13 | core
14 | end
15 |
16 | def reporter(arguments = {})
17 | core = CircuitSwitch::Builder.new
18 | core.assign_reporter(**arguments)
19 | core
20 | end
21 |
22 | def test_run_calls_block_when_all_conditions_are_clear
23 | test_value = 0
24 | runner.execute_run { test_value = 1 }
25 | assert_equal(
26 | 1,
27 | test_value
28 | )
29 | end
30 |
31 | def test_run_calls_updater_when_block_is_called_and_no_switch
32 | stub(CircuitSwitch::RunCountUpdater).perform_later(limit_count: nil, key: nil, called_path: called_path, reported: false, initially_closed: false)
33 | runner.execute_run {}
34 | assert_received(CircuitSwitch::RunCountUpdater) do |updator|
35 | updator.perform_later(limit_count: nil, key: nil, called_path: called_path, reported: false, initially_closed: false)
36 | end
37 | end
38 |
39 | def test_run_calls_block_when_close_if_reach_limit_is_false_and_reached_limit
40 | limit_count = 1
41 | CircuitSwitch::CircuitSwitch.new(run_limit_count: limit_count, caller: called_path, due_date: due_date).increment_run_count!
42 |
43 | test_value = 0
44 | runner(close_if_reach_limit: false).execute_run { test_value = 1 }
45 | assert_equal(
46 | 1,
47 | test_value
48 | )
49 | end
50 |
51 | def test_run_doesnt_call_block_when_close_if_reach_limit_is_true_and_reached_limit
52 | limit_count = 1
53 | CircuitSwitch::CircuitSwitch.new(run_limit_count: limit_count, caller: called_path, due_date: due_date).increment_run_count!
54 |
55 | test_value = 0
56 | runner(close_if_reach_limit: true).execute_run { test_value = 1 }
57 | assert_equal(
58 | 0,
59 | test_value
60 | )
61 | end
62 |
63 | def test_run_doesnt_call_block_when_run_is_terminated
64 | CircuitSwitch::CircuitSwitch.create(caller: called_path, due_date: due_date, run_is_terminated: true)
65 | test_value = 0
66 | runner.execute_run { test_value = 1 }
67 | assert_equal(
68 | 0,
69 | test_value
70 | )
71 | end
72 |
73 | def test_run_raises_error_when_close_if_reach_limit_is_true_and_reached_limit_is_zero
74 | assert_raise CircuitSwitch::CircuitSwitchError do
75 | runner(close_if_reach_limit: true, limit_count: 0).execute_run {}
76 | end
77 | end
78 |
79 | def test_run_doesnt_call_block_when_close_if_is_true
80 | test_value = 0
81 | runner(close_if: true).execute_run { test_value = 1 }
82 | assert_equal(
83 | 0,
84 | test_value
85 | )
86 | end
87 |
88 | def test_run_doesnt_call_block_when_if_is_false
89 | test_value = 0
90 | runner(if: false).execute_run { test_value = 1 }
91 | assert_equal(
92 | 0,
93 | test_value
94 | )
95 | end
96 |
97 | def test_run_calls_block_when_initially_closed_is_true_and_switch_exists
98 | CircuitSwitch::CircuitSwitch.create(caller: called_path, due_date: due_date)
99 | test_value = 0
100 | runner(initially_closed: true).execute_run { test_value = 1 }
101 | assert_equal(
102 | 1,
103 | test_value
104 | )
105 | end
106 |
107 | def test_run_doesnt_calls_block_when_initially_closed_is_true_and_no_switch
108 | test_value = 0
109 | runner(initially_closed: true).execute_run { test_value = 1 }
110 | assert_equal(
111 | 0,
112 | test_value
113 | )
114 | end
115 |
116 | def test_run_calls_updater_even_when_block_raises_error
117 | stub(CircuitSwitch::RunCountUpdater).perform_later(limit_count: nil, key: nil, called_path: called_path, reported: false, initially_closed: false)
118 | begin
119 | runner.execute_run { raise RuntimeError }
120 | rescue RuntimeError
121 | # noop
122 | end
123 | assert_received(CircuitSwitch::RunCountUpdater) do |updator|
124 | updator.perform_later(limit_count: nil, key: nil, called_path: called_path, reported: false, initially_closed: false)
125 | end
126 | end
127 |
128 | def test_report_reports_when_all_conditions_are_clear
129 | assert_equal(
130 | true,
131 | reporter.execute_report.reported?
132 | )
133 | end
134 |
135 | def test_report_reports_when_close_if_reach_limit_is_false_and_reached_limit
136 | limit_count = 1
137 | CircuitSwitch::CircuitSwitch.new(report_limit_count: limit_count, caller: called_path, due_date: due_date).increment_report_count!
138 | assert_equal(
139 | true,
140 | reporter(limit_count: limit_count, stop_report_if_reach_limit: false).execute_report.reported?
141 | )
142 | end
143 |
144 | def test_report_doesnt_report_when_close_if_reach_limit_is_true_and_reached_limit
145 | limit_count = 1
146 | CircuitSwitch::CircuitSwitch.new(report_limit_count: limit_count, caller: called_path, due_date: due_date).increment_report_count!
147 | stub(CircuitSwitch::Reporter).perform_later(limit_count: 10, called_path: called_path, run: false)
148 |
149 | assert_nothing_raised(RR::Errors::DoubleNotFoundError) do
150 | reporter(limit_count: 1, stop_report_if_reach_limit: true).execute_report
151 | end
152 | end
153 |
154 | def test_report_raises_error_when_reporter_is_nil
155 | stub(CircuitSwitch.config).reporter { nil }
156 | assert_raise CircuitSwitch::CircuitSwitchError do
157 | reporter.execute_report
158 | end
159 | end
160 |
161 | def test_report_raises_error_when_stop_report_if_reach_limit_is_true_and_reached_limit_is_zero
162 | assert_raise CircuitSwitch::CircuitSwitchError do
163 | reporter(stop_report_if_reach_limit: true, limit_count: 0).execute_report
164 | end
165 | end
166 |
167 | def test_report_doesnt_report_when_report_is_terminated
168 | CircuitSwitch::CircuitSwitch.create(caller: called_path, due_date: due_date, report_is_terminated: true)
169 |
170 | assert_equal(
171 | false,
172 | reporter.execute_report.reported?
173 | )
174 | end
175 |
176 | def test_report_doesnt_report_when_stop_report_if_is_true
177 | assert_equal(
178 | false,
179 | reporter(stop_report_if: true).execute_report.reported?
180 | )
181 | end
182 |
183 | def test_report_doesnt_report_when_if_is_false
184 | assert_equal(
185 | false,
186 | reporter(if: false).execute_report.reported?
187 | )
188 | end
189 |
190 | def test_report_doesnt_report_when_report_if_is_false
191 | stub(CircuitSwitch.config).enable_report? { false }
192 | assert_equal(
193 | false,
194 | reporter.execute_report.reported?
195 | )
196 | end
197 |
198 | def test_run_with_question_returns_true_when_run
199 | assert_equal(
200 | true,
201 | runner.execute_run {}.run?
202 | )
203 | end
204 |
205 | def test_run_with_question_returns_false_when_didnt_run
206 | assert_equal(
207 | false,
208 | runner(if: false).execute_run {}.run?
209 | )
210 | end
211 |
212 | def test_reported_with_question_returns_true_when_reported
213 | assert_equal(
214 | true,
215 | reporter.execute_report.reported?
216 | )
217 | end
218 |
219 | def test_reported_with_question_returns_false_when_didnt_report
220 | assert_equal(
221 | false,
222 | reporter(if: false).execute_report.reported?
223 | )
224 | end
225 | end
226 |
--------------------------------------------------------------------------------
/test/lib/orm/active_record/circuit_switch_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class CircuitSwitchTest < Test::Unit::TestCase
4 | def test_assign_sets_attribute_when_has_nil_and_receives_value
5 | circuit_switch = CircuitSwitch::CircuitSwitch
6 | .new(run_limit_count: nil)
7 | .assign(run_limit_count: 1)
8 | assert_equal(
9 | 1,
10 | circuit_switch.run_limit_count
11 | )
12 | end
13 |
14 | def test_assign_doesnt_set_attribute_when_has_nil_and_doesnt_receive_value
15 | circuit_switch = CircuitSwitch::CircuitSwitch
16 | .new(run_limit_count: nil)
17 | .assign
18 | assert_equal(
19 | nil,
20 | circuit_switch.run_limit_count
21 | )
22 | end
23 |
24 | def test_assign_sets_attribute_when_has_value_and_receives_value
25 | circuit_switch = CircuitSwitch::CircuitSwitch
26 | .new(run_limit_count: 1)
27 | .assign(run_limit_count: 2)
28 | assert_equal(
29 | 2,
30 | circuit_switch.run_limit_count
31 | )
32 | end
33 |
34 | def test_assign_doesnt_set_attribute_when_has_value_and_doesnt_receive_value
35 | circuit_switch = CircuitSwitch::CircuitSwitch
36 | .new(run_limit_count: 1)
37 | .assign
38 | assert_equal(
39 | 1,
40 | circuit_switch.run_limit_count
41 | )
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/test/lib/workers/due_date_notifier_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class DueDateNotifierTest < Test::Unit::TestCase
4 | include ::CircuitSwitch::TestHelper
5 |
6 | setup do
7 | CircuitSwitch::CircuitSwitch.truncate
8 | end
9 |
10 | def test_notifier_calls_config_notifier
11 | message = 'There is no switch!'
12 | assert_equal(
13 | message,
14 | CircuitSwitch::DueDateNotifier.new.perform
15 | )
16 | end
17 |
18 | def test_notifier_notifies_when_due_date_has_come
19 | due_date = Date.today
20 | circuit_switch = CircuitSwitch::CircuitSwitch.create(caller: called_path, due_date: due_date)
21 | message = "Due date has come! Let's consider about removing switches and cleaning up code! :)\n" +
22 | "id: #{circuit_switch.id}, key: '#{circuit_switch.caller}', caller: '#{circuit_switch.caller}', created_at: #{circuit_switch.created_at}"
23 | assert_equal(
24 | message,
25 | CircuitSwitch::DueDateNotifier.new.perform
26 | )
27 | end
28 |
29 | def test_notifier_notifies_when_due_date_does_not_come
30 | due_date = Date.today + 1
31 | CircuitSwitch::CircuitSwitch.create(caller: called_path, due_date: due_date)
32 | message = '1 switches are waiting for their due_date.'
33 | assert_equal(
34 | message,
35 | CircuitSwitch::DueDateNotifier.new.perform
36 | )
37 | end
38 |
39 | def test_notifier_raises_error_when_config_due_date_notifier_is_nil
40 | stub(CircuitSwitch.config).due_date_notifier { nil }
41 | assert_raise ::CircuitSwitch::CircuitSwitchError do
42 | CircuitSwitch::DueDateNotifier.new.perform
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/test/lib/workers/reporter_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class ReporterTest < Test::Unit::TestCase
4 | include ::CircuitSwitch::TestHelper
5 |
6 | setup do
7 | CircuitSwitch::CircuitSwitch.truncate
8 | end
9 |
10 | def test_reporter_calls_config_reporter
11 | message = CircuitSwitch::Reporter.new.perform(key: nil, limit_count: 10, called_path: called_path, stacktrace: [], run: false)
12 | assert_equal(
13 | CircuitSwitch::CircuitSwitch.last.message,
14 | message
15 | )
16 | end
17 |
18 | def test_reporter_creates_circuit_switch_when_no_switch
19 | assert_equal(
20 | 0,
21 | CircuitSwitch::CircuitSwitch.all.size
22 | )
23 | CircuitSwitch::Reporter.new.perform(key: nil, limit_count: 10, called_path: called_path, stacktrace: [], run: false)
24 | assert_equal(
25 | true,
26 | CircuitSwitch::CircuitSwitch.exists?(report_count: 1, key: called_path)
27 | )
28 | end
29 |
30 | def test_reporter_creates_circuit_switch_with_key_when_no_switch
31 | assert_equal(
32 | 0,
33 | CircuitSwitch::CircuitSwitch.all.size
34 | )
35 | CircuitSwitch::Reporter.new.perform(key: 'test', limit_count: 10, called_path: called_path, stacktrace: [], run: false)
36 | assert_equal(
37 | true,
38 | CircuitSwitch::CircuitSwitch.exists?(report_count: 1, key: 'test')
39 | )
40 | end
41 |
42 | def test_reporter_updates_circuit_switch_report_count_when_switch_exists
43 | CircuitSwitch::Reporter.new.perform(key: nil, limit_count: 10, called_path: called_path, stacktrace: [], run: false)
44 | assert_equal(
45 | 1,
46 | CircuitSwitch::CircuitSwitch.find_by(key: called_path, caller: called_path).report_count
47 | )
48 | CircuitSwitch::Reporter.new.perform(key: nil, limit_count: 10, called_path: called_path, stacktrace: [], run: false)
49 | assert_equal(
50 | 2,
51 | CircuitSwitch::CircuitSwitch.find_by(key: called_path, caller: called_path).report_count
52 | )
53 | end
54 |
55 | def test_reporter_updates_circuit_switch_report_count_with_key_when_switch_exists
56 | CircuitSwitch::Reporter.new.perform(key: 'test', limit_count: 10, called_path: called_path, stacktrace: [], run: false)
57 | assert_equal(
58 | 1,
59 | CircuitSwitch::CircuitSwitch.find_by(key: 'test').report_count
60 | )
61 | CircuitSwitch::Reporter.new.perform(key: 'test', limit_count: 10, called_path: called_path, stacktrace: [], run: false)
62 | assert_equal(
63 | 2,
64 | CircuitSwitch::CircuitSwitch.find_by(key: 'test', caller: called_path).report_count
65 | )
66 | end
67 |
68 | def test_reporter_raises_active_record_record_not_found_when_reported_and_no_switch_exists
69 | assert_raise ActiveRecord::RecordNotFound do
70 | CircuitSwitch::Reporter.new.perform(key: nil, limit_count: 10, called_path: called_path, stacktrace: [], run: true)
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/test/lib/workers/run_count_updater_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class RunCountUpdaterTest < Test::Unit::TestCase
4 | include ::CircuitSwitch::TestHelper
5 |
6 | setup do
7 | CircuitSwitch::CircuitSwitch.truncate
8 | end
9 |
10 | def test_updater_creates_circuit_switch_when_no_switch
11 | assert_equal(
12 | 0,
13 | CircuitSwitch::CircuitSwitch.all.size
14 | )
15 | CircuitSwitch::RunCountUpdater.new.perform(limit_count: 10, key: nil, called_path: called_path, reported: false, initially_closed: false)
16 | assert_equal(
17 | true,
18 | CircuitSwitch::CircuitSwitch.exists?(run_count: 1, key: called_path)
19 | )
20 | end
21 |
22 | def test_updater_creates_circuit_switch_with_key_when_no_switch
23 | assert_equal(
24 | 0,
25 | CircuitSwitch::CircuitSwitch.all.size
26 | )
27 | CircuitSwitch::RunCountUpdater.new.perform(limit_count: 10, key: 'test', called_path: called_path, reported: false, initially_closed: false)
28 | assert_equal(
29 | true,
30 | CircuitSwitch::CircuitSwitch.exists?(run_count: 1, key: 'test')
31 | )
32 | end
33 |
34 | def test_updater_updates_circuit_switch_run_count_when_switch_exists
35 | CircuitSwitch::RunCountUpdater.new.perform(limit_count: 10, key: nil, called_path: called_path, reported: false, initially_closed: false)
36 | assert_equal(
37 | 1,
38 | CircuitSwitch::CircuitSwitch.find_by(key: called_path, caller: called_path).run_count
39 | )
40 | CircuitSwitch::RunCountUpdater.new.perform(limit_count: 10, key: nil, called_path: called_path, reported: false, initially_closed: false)
41 | assert_equal(
42 | 2,
43 | CircuitSwitch::CircuitSwitch.find_by(key: called_path, caller: called_path).run_count
44 | )
45 | end
46 |
47 | def test_updater_updates_circuit_switch_run_count_with_key_when_switch_exists
48 | CircuitSwitch::RunCountUpdater.new.perform(limit_count: 10, key: 'test', called_path: called_path, reported: false, initially_closed: false)
49 | assert_equal(
50 | 1,
51 | CircuitSwitch::CircuitSwitch.find_by(key: 'test').run_count
52 | )
53 | CircuitSwitch::RunCountUpdater.new.perform(limit_count: 10, key: nil, called_path: called_path, reported: false, initially_closed: false)
54 | assert_equal(
55 | 2,
56 | CircuitSwitch::CircuitSwitch.find_by(key: 'test', caller: called_path).run_count
57 | )
58 | end
59 |
60 | def test_updater_raises_active_record_record_not_found_when_reported_and_no_switch_exists
61 | assert_raise ActiveRecord::RecordNotFound do
62 | CircuitSwitch::RunCountUpdater.new.perform(limit_count: 10, key: nil, called_path: called_path, reported: true, initially_closed: false)
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/test/support/setup_database.rb:
--------------------------------------------------------------------------------
1 | if ActiveRecord.version >= Gem::Version.new('6.0')
2 | ActiveRecord::Base.configurations = { test: { adapter: 'sqlite3', database: ':memory:' } }
3 | ActiveRecord::Base.establish_connection :test
4 | else
5 | ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
6 | end
7 |
8 | class CreateCircuitSwitches < ActiveRecord::Migration[5.0]
9 | def self.up
10 | create_table :circuit_switches do |t|
11 | t.string :awesome_key, null: false
12 | t.string :caller, null: false
13 | t.integer :run_count, default: 0, null: false
14 | t.integer :run_limit_count, default: 10, null: false
15 | t.boolean :run_is_terminated, default: false, null: false
16 | t.integer :report_count, default: 0, null: false
17 | t.integer :report_limit_count, default: 10, null: false
18 | t.boolean :report_is_terminated, default: false, null: false
19 | t.date :due_date, null: false
20 | t.timestamps
21 | end
22 | add_index :circuit_switches, [:awesome_key], unique: true
23 | end
24 | end
25 |
26 | ActiveRecord::Migration.verbose = false
27 | CreateCircuitSwitches.up
28 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
2 |
3 | require 'active_job'
4 | require 'active_record'
5 |
6 | require 'bundler/setup'
7 | Bundler.require
8 |
9 | require 'support/setup_database'
10 |
11 | require 'byebug'
12 | require 'test/unit'
13 | require 'test/unit/rr'
14 | require 'circuit_switch'
15 |
16 | CircuitSwitch.configure do |config|
17 | config.reporter = -> (message, error) { DummyReporter.report(message) }
18 | config.report_paths = [Dir.pwd]
19 | config.report_if = true
20 | config.key_column_name = :awesome_key
21 | config.due_date_notifier = -> (message) { DummyReporter.report(message) }
22 | end
23 |
24 | class DummyReporter
25 | def self.report(message)
26 | message
27 | end
28 | end
29 |
30 | class CircuitSwitch::CircuitSwitch < ActiveRecord::Base
31 | def self.truncate
32 | connection.execute 'DELETE FROM circuit_switches'
33 | end
34 | end
35 |
36 | module CircuitSwitch::TestHelper
37 | def called_path
38 | "/somewhere/in/library:in #{Date.today}"
39 | end
40 |
41 | def due_date
42 | Date.today + 10
43 | end
44 | end
45 |
46 | ActiveJob::Base.queue_adapter = :test
47 |
--------------------------------------------------------------------------------