-
Notifications
You must be signed in to change notification settings - Fork 87
Add adaptive circuit breaker #760
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AbdulRahmanAlHamali
wants to merge
164
commits into
main
Choose a base branch
from
pid-take-2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+7,594
−366
Open
Changes from all commits
Commits
Show all changes
164 commits
Select commit
Hold shift + click to select a range
4ef2649
add adaptive circuit breaker
AbdulRahmanAlHamali b19e198
add ability to enable adaptive circuit breaker
AbdulRahmanAlHamali 824f229
fix constructor
AbdulRahmanAlHamali d14c397
address deadlock issues and use standard clamp function
AbdulRahmanAlHamali fec549a
add time freeze to tests
AbdulRahmanAlHamali 23f6ec8
remove unnecessary stubbing
AbdulRahmanAlHamali ef7ec52
cap ideal error rate, and fix tests
AbdulRahmanAlHamali 54e036e
more test fixing
AbdulRahmanAlHamali e037ead
use a discrete time window in pid controller
AbdulRahmanAlHamali 3f59687
add unprotected ping
AbdulRahmanAlHamali ef08850
fix bugs and improve example
AbdulRahmanAlHamali 4af0551
add ability to enable adaptive circuit breaker
AbdulRahmanAlHamali 9ce68c6
use a discrete time window in pid controller
AbdulRahmanAlHamali 9f4357e
Add P2 estimator
kris-gaudel 6795d2e
Use P2 estimatori in PID
kris-gaudel e1e055c
Add results
kris-gaudel 2c64138
Update variable names
kris-gaudel 6bf05dd
Interpolate instead of round
kris-gaudel 56c1db0
Add missing case
kris-gaudel 2e43221
Remove platform
kris-gaudel 645880d
Check if 1 hr has elapsed before changing ideal error rate (#800)
nirmitparikh8 4a6f482
Fix cold start issue (#809)
nirmitparikh8 86a733e
Remove outputs
kris-gaudel 56ed006
testing different circuit breaking scenarios (#794)
Aguasvivas22 90cefeb
remove unintentional change
AbdulRahmanAlHamali f96df80
remove AI fluff
AbdulRahmanAlHamali 3546cf1
use standard "error function" name instead of health
AbdulRahmanAlHamali 814c5ed
remove pings from the error function
AbdulRahmanAlHamali 18f4ed0
remove unintentionally commited change
AbdulRahmanAlHamali 7961ecc
Inherit from `CircuitBreaker`
kris-gaudel 2b6fc8d
Appease linter
kris-gaudel d40fa7a
Revert "Appease linter"
kris-gaudel 129a595
Automatic lint
kris-gaudel f907d59
Comment out broken tests for ACB
kris-gaudel f68c1df
Skip broken test
kris-gaudel cbf0cab
Add behaviour
kris-gaudel b4caf65
Remove params not needed
kris-gaudel 47dbd90
Merge pull request #812 from Shopify/kris-gaudel/cb-class-inherit
kris-gaudel 45bef95
separate the service from the adapter (#817)
AbdulRahmanAlHamali ab36f2f
add test for gradual increase in errors
Aguasvivas22 bf47253
add test for oscillating errors
Aguasvivas22 f1e05f3
Merge pull request #819 from Shopify/pid-take-2-experiments-3
Aguasvivas22 ea7ed59
Merge pull request #818 from Shopify/pid-take-2-experiments-2
Aguasvivas22 a369832
Use floating point arithmetic
kris-gaudel 222a8c6
Automatic lint
kris-gaudel d62da4d
Add demo script
kris-gaudel 060279d
Unit test for P2 estimator
kris-gaudel f3e0873
Create helper class and refactor tests
Aguasvivas22 e55f44f
Merge pull request #823 from Shopify/cb_helper
Aguasvivas22 5a2553c
Clean up PID Controller, and fix its tests (#820)
AbdulRahmanAlHamali 0600df6
add new tests for sudden spikes
adriangudas 0e27576
add classic, and adaptive, tests for error spikes of different sizes
adriangudas 196340d
Add p90 demo
kris-gaudel f191d09
Merge branch 'pid-take-2' into kris-gaudel/p2-estimator-tests
kris-gaudel 61a0bf2
Merge pull request #822 from Shopify/kris-gaudel/p2-estimator-tests
kris-gaudel 384adea
fix 0.01% typo (should be 1%)
adriangudas ab29e63
add "sudden error spikes" experiments and results #824
adriangudas d1e10e5
fix adaptive circuit breaker tests
AbdulRahmanAlHamali 4fceeb4
Automatic fixes from rubucop
kris-gaudel 5e29f52
fix adaptive circuit breaker tests (#828)
AbdulRahmanAlHamali caccf15
Remove clock dep inj
kris-gaudel 966ed74
Merge branch 'main' into pid-take-2
AbdulRahmanAlHamali 4f9de61
fix linting error (#830)
AbdulRahmanAlHamali 41c8648
re-add attr_reader so experiments can report on rejection rate
adriangudas f872dd2
expose request_rate attribute (fixes PR 820) #832
adriangudas 629ab08
Add thread timing utilization metrics for experiments (#829)
adriangudas 00d11b4
add a lower bound integral windup test
Aguasvivas22 02df23f
Merge pull request #838 from Shopify/test_lower_bound_windup
Aguasvivas22 df37c03
Merge branch 'pid-take-2' into fix-adaptive-circuit-breaker-tests
kris-gaudel 1bb1004
Semian per thread pattern
kris-gaudel 8ae141b
Update gemfile
kris-gaudel 86637e3
increase error threshold
kris-gaudel 80549de
Update gemfile
kris-gaudel e5129f5
anti-windup
kris-gaudel bf43c4d
wind up test
kris-gaudel d67cbce
Clamp on saturation
kris-gaudel 1dd0fdb
Fix test
kris-gaudel f2f662d
Add an experiment with multiple services and latency degradation of o…
AbdulRahmanAlHamali 88dccaf
Update images
kris-gaudel 2ad64db
Fix "error P" reporting in summary output (#839)
adriangudas 0860b25
Cleanup parameters
kris-gaudel dc563e8
Address race condition
kris-gaudel 3a6470e
Clean up comments
kris-gaudel b1b0378
notify on state transitions and on controller updates
Aguasvivas22 816b123
Same ideal error
kris-gaudel 28eb197
Merge branch 'pid-take-2' into kris-gaudel/diff-semians-diff-threads
kris-gaudel 428ffff
subscribe to notificaton to replace polling thread
Aguasvivas22 7097171
Merge pull request #849 from Shopify/notify_adapative_changes
Aguasvivas22 efed14a
Merge branch 'pid-take-2' into fix-adaptive-circuit-breaker-tests
kris-gaudel eb990bd
add vertical lines on classic CB state changes and more helper flexib…
Aguasvivas22 5509817
Merge pull request #853 from Shopify/recalc_error_rate
Aguasvivas22 11e99e3
Merge branch 'pid-take-2' into fix-adaptive-circuit-breaker-tests
kris-gaudel cfd3222
Lint, fix Fernando's tests to not use mock clock
kris-gaudel 10aca61
Merge pull request #845 from Shopify/fix-adaptive-circuit-breaker-tests
kris-gaudel abcd264
Merge branch 'pid-take-2' into kris-gaudel/integral-windup-v2
kris-gaudel 60dfcb6
Add comment
kris-gaudel dac8441
Merge pull request #847 from Shopify/kris-gaudel/integral-windup-v2
kris-gaudel fdd251e
Merge branch 'pid-take-2' into kris-gaudel/diff-semians-diff-threads
kris-gaudel 4e8f39c
Revert error threshold
kris-gaudel da357cc
Add service name
kris-gaudel 096c11f
Remove images
kris-gaudel 8865e6c
Monitoring
kris-gaudel 67ee84d
Add runtime dep to suppress warning msg
kris-gaudel 5fcba93
Update lock file
kris-gaudel 804d8d4
Update lock
kris-gaudel ff910f2
Remove dep from main semian
kris-gaudel ba9269f
Only add logger dep to experiments
kris-gaudel 0cf0d43
Remove `window_number`
kris-gaudel 90dd815
Add image back
kris-gaudel 861e89c
Fix aggregate metrics
kris-gaudel 2cf2f23
Merge pull request #846 from Shopify/kris-gaudel/diff-semians-diff-th…
kris-gaudel cb2690f
Add automated workflow for pid (#855)
nirmitparikh8 83779e3
Introduce a slow query experiment (#857)
AbdulRahmanAlHamali fe69b9f
Commit experiment result tables and fix bug in experiment helper (#872)
AbdulRahmanAlHamali f32fc05
Replace p90 and P2Estimator with Simple Exponential Smoother (SES) (#…
kris-gaudel a2fc6fb
Remove Throughput and Duration Graphs
Aguasvivas22 86bd5fd
regenerating graphs
Aguasvivas22 3668177
add test which holds the error rate near the target error rate
Aguasvivas22 5c17afc
Merge pull request #884 from Shopify/remove_throughput_duration_graphs
Aguasvivas22 447faeb
Merge pull request #887 from Shopify/near_target_error_rate
Aguasvivas22 60ee6a5
improve experiement visualization (#892)
AbdulRahmanAlHamali a7d7a57
Sliding window implementation for PID controller (#874)
AbdulRahmanAlHamali 4c34eff
fix experiments that were not working (#896)
AbdulRahmanAlHamali 6a3963d
Remove unnecessary comments
kris-gaudel d1d1d28
Remove unnecessary variables and init smoother to 5%
kris-gaudel deceebb
Update experiment results
kris-gaudel facd604
Merge pull request #897 from Shopify/kris-gaudel/set-initial-er
kris-gaudel b495564
Add Elastic Defensiveness (#899)
AbdulRahmanAlHamali a72d41f
dual circuit breaker implementation for switching between adaptive an…
adriangudas be0d3a2
Merge branch 'main' into pid-take-2
AbdulRahmanAlHamali f8db2ef
fix demo, and delete redundant demo
AbdulRahmanAlHamali 2dd2a87
fix linter failures
AbdulRahmanAlHamali 6d593a3
rerun experiments
AbdulRahmanAlHamali 70cff45
create a class instead of a module for ExperimentFlags
adriangudas 2948cef
Revert "create a class instead of a module for ExperimentFlags"
adriangudas c53d656
Merge branch 'main' into pid-take-2
AbdulRahmanAlHamali 7d09e32
rerun experiments
AbdulRahmanAlHamali ce5f917
Jan 5: add tests, and fix demo (#961)
adriangudas 77cb2b0
Allow using the fiber scheduler if present (#972)
AbdulRahmanAlHamali 7a4c941
Update PID controller to store and use last ideal error rate for impr…
Abuudiii 89d8752
Added CSV and PNG files for all experiments ran.
Abuudiii d873937
Merge pull request #983 from Shopify/abdullah/fixed-redundant-ideal-e…
Abuudiii 4b8c4c6
Optimized metric usage in adaptive circuit breaker (#985)
Abuudiii ebdd1f8
Add jitter to sleep (#990)
AbdulRahmanAlHamali e39603f
initial commit
adriangudas a753114
don't use a separate WorkerState object
adriangudas 537204f
use a class variable so that it exists before the class instance is c…
adriangudas a33e259
re-run experiments
adriangudas a2e865f
clean up terminology
adriangudas 97973a9
reduce code duplication in dual circuit breaker
adriangudas cef49bb
add experiments
adriangudas d8738ed
remove the ability to stop the update thread (for now)
adriangudas 53f889d
attempt to fix tests
adriangudas e36461f
stop the thread if all circuit breakers are destroyed (fixes tests)
adriangudas 0a495da
make sure initialize doesn't run twice
adriangudas af67ec2
add experiment results
adriangudas 43c2b0b
working single thread implementation
adriangudas 8837cad
merge in latest metrics optimizations, and fix tests
adriangudas 440f4d0
revert changes to pid_controller_test
adriangudas 2be4606
re-run experiments
adriangudas 8ff192a
use fibers if SEMIAN_PID_CONTROLLER_USE_FIBERS is true
adriangudas 02d22cb
add experiments
adriangudas 6f4df13
Merge pull request #973 from Shopify/adriangudas/adaptive-cb-single-t…
adriangudas c9eef57
Add dead zone ratio to PID controller for noise suppression (#1032)
Abuudiii File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| --- | ||
| name: Automated Experiment Result Checker | ||
|
|
||
| # yamllint disable-line rule:truthy | ||
| on: | ||
| pull_request: | ||
| types: [opened, reopened, synchronize] | ||
|
|
||
| concurrency: | ||
| group: ${{ github.ref }}-automated-experiment-result-checker | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
| jobs: | ||
| automated-experiment-result-checker: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| with: | ||
| ref: ${{ github.event.pull_request.head.sha }} | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Check for updated experiment result graphs and tables | ||
| run: | | ||
| set -e | ||
| cd "$(git rev-parse --show-toplevel)" | ||
|
|
||
| # TODO: Include lower bound windup experiment once we have a way to make it run in a reasonable time. | ||
| # Find all PNGs and CSV files, excluding those with "windup" in their filename | ||
| mapfile -t all_pngs < <(find experiments/results/main_graphs -type f -name '*.png' ! -name '*windup*.png' | sort) | ||
| mapfile -t all_csvs < <(find experiments/results/csv -type f -name '*.csv' ! -name '*windup*.csv' 2>/dev/null | sort) | ||
|
|
||
| # Combine all result files | ||
| all_files=("${all_pngs[@]}" "${all_csvs[@]}") | ||
|
|
||
| # Find all changed PNGs and CSVs in the latest commit | ||
| mapfile -t changed_files < <(git diff --name-only --diff-filter=AM HEAD~1..HEAD | grep -E '^experiments/results/main_graphs/.*\.png$|^experiments/results/csv/.*\.csv$' | grep -v windup | sort) | ||
|
|
||
| # Report any files that are not updated in the latest commit | ||
| declare -a not_updated=() | ||
| for file in "${all_files[@]}"; do | ||
| if ! printf "%s\n" "${changed_files[@]}" | grep -qx "$file"; then | ||
| not_updated+=("$file") | ||
| fi | ||
| done | ||
|
|
||
| if [ ${#not_updated[@]} -gt 0 ]; then | ||
| echo "❌ The following result files have NOT been updated in the latest commit:" | ||
| for f in "${not_updated[@]}"; do | ||
| echo " - $f" | ||
| done | ||
| echo "" | ||
| echo "Every commit must update all non-windup experiment result graphs and CSV files. You may be missing updates." | ||
| echo "Run:" | ||
| echo "" | ||
| echo " cd experiments" | ||
| echo " bundle install" | ||
| echo " bundle exec ruby run_all_experiments.rb" | ||
| echo "" | ||
| echo "Commit the updated graphs and CSV files to resolve this check." | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "✅ All non-windup experiment result graphs and CSV files are up to date for this commit!" | ||
|
|
||
|
|
||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| #!/usr/bin/env ruby | ||
| # frozen_string_literal: true | ||
|
|
||
| require "bundler/setup" | ||
| require "semian" | ||
|
|
||
| # Example: Dual Circuit Breaker Demo | ||
| # This demonstrates how to use both legacy and adaptive circuit breakers | ||
| # simultaneously, switching between them at runtime based on a callable. | ||
|
|
||
| # Simulate a feature flag that can be toggled | ||
| class ExperimentFlags | ||
| @enabled = false | ||
|
|
||
| def enable_adaptive! | ||
| @enabled = true | ||
| end | ||
|
|
||
| def disable_adaptive! | ||
| @enabled = false | ||
| end | ||
|
|
||
| def use_adaptive_circuit_breaker? | ||
| @enabled | ||
| end | ||
| end | ||
|
|
||
| # Helper function to print state of all Semian objects between each phase | ||
| def print_semian_state | ||
| puts "\n=== Semian Resources State ===\n" | ||
| Semian.resources.values.each do |resource| | ||
| puts "Resource: #{resource.name}" | ||
|
|
||
| # Bulkhead info | ||
| if resource.bulkhead | ||
| puts " Bulkhead: tickets=#{resource.tickets}, count=#{resource.count}" | ||
| else | ||
| puts " Bulkhead: disabled" | ||
| end | ||
|
|
||
| # Circuit breaker info | ||
| cb = resource.circuit_breaker | ||
| if cb.nil? | ||
| puts " Circuit Breaker: disabled" | ||
| elsif cb.is_a?(Semian::DualCircuitBreaker) | ||
| puts " Circuit Breaker: DualCircuitBreaker" | ||
| metrics = cb.metrics | ||
| puts " Active: #{metrics[:active]}" | ||
| puts " Classic: state=#{metrics[:classic][:state]}, open=#{metrics[:classic][:open]}, half_open=#{metrics[:classic][:half_open]}" | ||
| puts " Adaptive: rejection_rate=#{metrics[:adaptive][:rejection_rate]}, error_rate=#{metrics[:adaptive][:error_rate]}" | ||
| elsif cb.is_a?(Semian::AdaptiveCircuitBreaker) | ||
| puts " Circuit Breaker: AdaptiveCircuitBreaker" | ||
| puts " open=#{cb.open?}, closed=#{cb.closed?}, half_open=#{cb.half_open?}" | ||
| else | ||
| puts " Circuit Breaker: Legacy" | ||
| puts " state=#{cb.state&.value}, open=#{cb.open?}, closed=#{cb.closed?}, half_open=#{cb.half_open?}" | ||
| puts " last_error=#{cb.last_error&.class}" | ||
| end | ||
| puts "" | ||
| end | ||
| puts "=== END STATE OUTPUT ===\n\n" | ||
| end | ||
|
|
||
| # Register a resource with dual circuit breaker mode | ||
| resource = Semian.register( | ||
| :my_service, | ||
| # Enable dual circuit breaker mode | ||
| dual_circuit_breaker: true, | ||
|
|
||
| # Legacy circuit breaker parameters (required) | ||
| success_threshold: 2, | ||
| error_threshold: 3, | ||
| error_timeout: 10, | ||
|
|
||
| # Adaptive circuit breaker parameters (optional, has defaults) | ||
| seed_error_rate: 0.01, | ||
|
|
||
| # Common parameters | ||
| tickets: 5, | ||
| timeout: 0.5, | ||
| exceptions: [RuntimeError], | ||
| ) | ||
|
|
||
| experiment_flags = ExperimentFlags.new | ||
| Semian::DualCircuitBreaker.adaptive_circuit_breaker_selector(->(_resource) { experiment_flags.use_adaptive_circuit_breaker? }) | ||
|
|
||
| puts "=== Dual Circuit Breaker Demo ===\n\n" | ||
|
|
||
| # Helper to simulate service calls | ||
| def simulate_call(success: true) | ||
| if success | ||
| "Success!" | ||
| else | ||
| raise "Service error" | ||
| end | ||
| end | ||
|
|
||
| # Test with legacy circuit breaker (use_adaptive returns false) | ||
| puts "Phase 1: Using LEGACY circuit breaker (use_adaptive=false)" | ||
| puts "The first 3 requests will succeed, the rest will fail." | ||
| puts "-" * 50 | ||
|
|
||
| experiment_flags.disable_adaptive! | ||
|
|
||
| 10.times do |i| | ||
| result = Semian[:my_service].acquire do | ||
| simulate_call(success: i < 3) # First 3 succeed, rest fail | ||
| end | ||
| puts " Request #{i + 1}: #{result}" | ||
| rescue => e | ||
| puts " Request #{i + 1}: Failed - #{e.class.name}: #{e.message}" | ||
| end | ||
|
|
||
| print_semian_state | ||
|
|
||
| # Reset both circuit breakers | ||
| puts "\n" + "=" * 50 | ||
| puts "Resetting circuit breakers..." | ||
| resource.circuit_breaker.reset | ||
|
|
||
| # Test with adaptive circuit breaker (use_adaptive returns true) | ||
| puts "\nPhase 2: Using ADAPTIVE circuit breaker (use_adaptive=true)" | ||
| puts "The first 3 requests will succeed, then the rest will be failures." | ||
| puts "The adaptive circuit breaker is not expected to open yet." | ||
| puts "-" * 50 | ||
|
|
||
| experiment_flags.enable_adaptive! | ||
|
|
||
| 10.times do |i| | ||
| begin | ||
| result = Semian[:my_service].acquire do | ||
| simulate_call(success: i < 3) # First 3 succeed, rest fail | ||
| end | ||
| puts " Request #{i + 1}: #{result}" | ||
| rescue => e | ||
| puts " Request #{i + 1}: Failed - #{e.class.name}: #{e.message}" | ||
| end | ||
| sleep 0.05 # Small delay to see adaptive behavior | ||
| end | ||
|
|
||
| print_semian_state | ||
|
|
||
| # Demonstrate dynamic switching | ||
| puts "\n" + "=" * 50 | ||
| puts "Phase 3: Dynamic switching between circuit breakers" | ||
| puts "-" * 50 | ||
|
|
||
| 5.times do |i| | ||
| # Toggle every 2 requests | ||
| if i.even? | ||
| experiment_flags.disable_adaptive! | ||
| puts " Switched to LEGACY" | ||
| else | ||
| experiment_flags.enable_adaptive! | ||
| puts " Switched to ADAPTIVE" | ||
| end | ||
|
|
||
| begin | ||
| result = Semian[:my_service].acquire do | ||
| simulate_call(success: true) | ||
| end | ||
| puts " Request #{i + 1}: #{result}" | ||
| rescue => e | ||
| puts " Request #{i + 1}: Failed - #{e.class.name}" | ||
| end | ||
| end | ||
|
|
||
| puts "\n=== Demo Complete ===\n" | ||
| puts "Both circuit breakers tracked all requests, but only the active one" | ||
| puts "was used for decision-making based on the adaptive_circuit_breaker_selector callable." | ||
|
|
||
| print_semian_state | ||
|
|
||
| # Cleanup | ||
| Semian.destroy(:my_service) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Binary file not shown.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.