├── .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 | ![GUI](circuit_switch.png) 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 | 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 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | <% @circuit_switches.each do |circuit_switch| %> 37 | 38 | 39 | 40 | 41 | 42 | 49 | 56 | 61 | 66 | 67 | <% end %> 68 | 69 |
idkeycallerdue_daterunreportshow/editdestroy
<%= circuit_switch.id %><%= circuit_switch.key %><%= circuit_switch.caller %><%= circuit_switch.due_date %> 43 | <% if circuit_switch.run_is_terminated %> 44 | 45 | <% else %> 46 | 47 | <% end %> 48 | 50 | <% if circuit_switch.report_is_terminated %> 51 | 52 | <% else %> 53 | 54 | <% end %> 55 | 57 | <%= button_to edit_circuit_switch_path(circuit_switch), method: :get, class: 'btn btn-outline-dark' do %> 58 | 59 | <% end %> 60 | 62 | <%= button_to circuit_switch_path(circuit_switch), method: :delete, class: 'btn btn-danger btn-destroy' do %> 63 | 64 | <% end %> 65 |
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 | --------------------------------------------------------------------------------