├── README.md ├── dont-log-secrets ├── python │ ├── README.md │ └── example1.py └── ruby │ ├── README.md │ ├── bad-example.rb │ ├── example1.rb │ ├── example2.rb │ └── lib │ ├── secret1.rb │ └── secret2.rb ├── error-reporting └── ruby │ └── README.md ├── interval-execution └── ruby │ ├── README.md │ ├── examples │ ├── interval1.rb │ ├── interval2.rb │ └── interval3.rb │ ├── graph.png │ └── run.rb ├── randomized-stress-testing └── ruby │ ├── examples │ ├── analyze_number.rb │ ├── socket_acceptance_spec.rb │ ├── socket_analyze_spec.rb │ └── socket_stress_spec.rb │ ├── lib │ ├── randomized.rb │ └── rspec │ │ └── stress_it.rb │ └── spec │ └── randomized_spec.rb ├── resource-pool └── ruby │ ├── README.md │ ├── example.rb │ └── lib │ └── pool.rb ├── retry-on-failure └── ruby │ ├── README.md │ ├── example-sequel.rb │ ├── example.rb │ └── lib │ ├── http.rb │ └── try.rb └── supervising-threads └── ruby ├── lib └── supervise.rb └── test.rb /README.md: -------------------------------------------------------------------------------- 1 | # Common Software Patterns 2 | 3 | Explorations in basic software patterns. 4 | -------------------------------------------------------------------------------- /dont-log-secrets/python/README.md: -------------------------------------------------------------------------------- 1 | # Hiding secrets in Python 2 | 3 | The purpose of this 'hiding' is documented [here, in the ruby implementation 4 | readme](https://github.com/jordansissel/software-patterns/tree/master/dont-log-secrets/ruby/README.md) 5 | 6 | % python example1.py 7 | str(): 8 | repr(): '' 9 | .value(): nobody will guess me 10 | json.dumps(): {"q": "hello world", "password": "", "user": "jordan"} 11 | 12 | As you can see above, you won't accidentally ship the secret to a logfile or 13 | other debugging tool. 14 | -------------------------------------------------------------------------------- /dont-log-secrets/python/example1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | from json import JSONEncoder 5 | 6 | # Hide a secret value from loggers and json encoders. 7 | # This is to prevent accidental exposure of password fields. 8 | # 9 | # To get the value of this secret, call secret.value() 10 | # Otherwise, repr() and str() on these objects will return 11 | # simply "" 12 | class Secret(object): 13 | def __init__(self, value): 14 | # Define a method to hide the value. This uses the 15 | # technique described here: 16 | # https://github.com/jordansissel/software-patterns/blob/master/dont-log-secrets/ruby/README.md#implementation-2-hiding-the-instance-variable 17 | def valuefunc(): 18 | return value 19 | self.value = valuefunc 20 | 21 | def __repr__(self): 22 | return "" 23 | 24 | def __str__(self): 25 | return repr(self) 26 | 27 | # Provide a custom 'default' encoder method to cover 28 | # objects of type 'Secret' 29 | jsenc = JSONEncoder() 30 | def secretjson(obj): 31 | if isinstance(obj, Secret): 32 | return repr(obj) 33 | return jsenc.default(obj) 34 | 35 | 36 | params = { 37 | "q": "hello world", 38 | "user": "jordan", 39 | "password": "nobody will guess me" 40 | } 41 | 42 | # Override the 'password' param as a Secret. 43 | # this would be common if you get your params from wsgi, for example. 44 | params["password"] = Secret(params["password"]) 45 | 46 | print "str(): %s" % params["password"] 47 | print "repr(): %r" % repr(params["password"]) 48 | print ".value(): %s" % params["password"].value() 49 | print "json.dumps(): %s" % json.dumps(params, default=secretjson) 50 | -------------------------------------------------------------------------------- /dont-log-secrets/ruby/README.md: -------------------------------------------------------------------------------- 1 | # Preventing Accidental Secret Leaks 2 | 3 | Application logs are often riddled with passwords. Oops, right? 4 | 5 | Your 'User' model might have a password field, and you might just do this: 6 | ``` 7 | logger.info(user) 8 | ``` 9 | 10 | ``` 11 | I, [2012-06-08T18:11:03.393290 #1154] INFO -- : # 12 | ``` 13 | 14 | Oops. You just leaked the password value if it is an instance variable. 15 | 16 | ## Goal 17 | 18 | The goal here is not to prevent malicious activity, but to prevent accidental 19 | activity, such as logging, from exposing secret information. 20 | 21 | ## Implementation 1: Wrapping a value. 22 | 23 | In this implementation, I simply use a class to wrap a value. The class 24 | provides `to_s` and `inspect` methods to prevent accidental exposure. 25 | 26 | The goal is to prevent any unintentional access to the secret value. To that 27 | end, we want to make any intentional access quite explicit. In this case, you 28 | must call `Secret#value` in order to get the original value. 29 | 30 | See [secret1.rb](https://github.com/jordansissel/software-patterns/blob/master/dont-log-secrets/ruby/lib/secret1.rb) for the class definition. 31 | 32 | This was written based on recognition that loggers, printing, and object 33 | inspection can often reveal internals of an object you would prefer 34 | not having exposed. 35 | 36 | The code change required is that you wrap any secrets with a Secret class. 37 | You'll need to make any secret access explicit, calling secret.value, etc. 38 | 39 | Example results: 40 | 41 | ``` 42 | #> 43 | I, [2012-06-08T13:41:30.216136 #13346] INFO -- : #> 44 | ``` 45 | 46 | As you can see above, we hide the password in both cases. 47 | 48 | There are more powerful inspection tools, like awesome_print, that will leak 49 | your secrets, though. 50 | 51 | ``` 52 | >> ap jordan 53 | # 58 | > 59 | ``` 60 | 61 | We'll need to fix this. 62 | 63 | ## Implementation 2: Hiding the instance variable 64 | 65 | Instead of using an instance variable, just define a method that returns the value. 66 | 67 | See [secret2.rb](https://github.com/jordansissel/software-patterns/blob/master/dont-log-secrets/ruby/lib/secret2.rb) for the class definition. 68 | 69 | ``` 70 | % ruby example2.rb 71 | I, [2012-06-08T14:03:17.685723 #13914] INFO -- : #> 72 | # 75 | > 76 | The secret: my password 77 | ``` 78 | 79 | Now above, you see all 3 cases the secret is hidden, but still accessible if I 80 | am explicit in asking for the secret value. 81 | 82 | 83 | ## Why does this work? 84 | 85 | * When you use `puts some_object` invokes 'some_object.to_s' to emit the value. 86 | * When you use `p some_object` it invokes `some_object.inspect` 87 | * Tools like ruby's Logger and awesome_print inspect the object for instance 88 | variables before emitting. 89 | -------------------------------------------------------------------------------- /dont-log-secrets/ruby/bad-example.rb: -------------------------------------------------------------------------------- 1 | # This is an example of a common case. Holding a secret in an instance variable 2 | # of an object, then printing or logging that object. 3 | class User 4 | attr_accessor :name 5 | attr_accessor :password 6 | 7 | def initialize(name, password) 8 | @name = name 9 | @password = password 10 | end 11 | end # def User 12 | 13 | # Example with 'inspect' 14 | jordan = User.new("jordan", "my password") 15 | 16 | require "logger" 17 | logger = Logger.new(STDOUT) 18 | logger.info(jordan) 19 | -------------------------------------------------------------------------------- /dont-log-secrets/ruby/example1.rb: -------------------------------------------------------------------------------- 1 | require "ap" 2 | require "./lib/secret1" 3 | 4 | # This is an example of a common case. Holding a secret in an instance variable 5 | # of an object, then printing or logging that object. 6 | class User 7 | attr_accessor :name 8 | attr_accessor :password 9 | 10 | def initialize(name, password) 11 | @name = name 12 | @password = Secret.new(password) 13 | end 14 | end # def User 15 | 16 | # Example with 'inspect' 17 | jordan = User.new("jordan", "my password") 18 | puts jordan.inspect 19 | 20 | require "logger" 21 | logger = Logger.new(STDOUT) 22 | logger.info(jordan) 23 | 24 | ap jordan 25 | -------------------------------------------------------------------------------- /dont-log-secrets/ruby/example2.rb: -------------------------------------------------------------------------------- 1 | require "ap" 2 | require "./lib/secret2" 3 | 4 | # This is an example of a common case. Holding a secret in an instance variable 5 | # of an object, then printing or logging that object. 6 | class User 7 | attr_accessor :name 8 | attr_accessor :password 9 | 10 | def initialize(name, password) 11 | @name = name 12 | @password = Secret2.new(password) 13 | end 14 | end # def User 15 | 16 | # Example with 'inspect' 17 | jordan = User.new("jordan", "my password") 18 | puts jordan.inspect 19 | 20 | require "logger" 21 | logger = Logger.new(STDOUT) 22 | logger.info(jordan) 23 | 24 | ap jordan 25 | 26 | puts "The secret: #{jordan.password.value}" 27 | -------------------------------------------------------------------------------- /dont-log-secrets/ruby/lib/secret1.rb: -------------------------------------------------------------------------------- 1 | # A class for holding a secret. The main goal is to prevent the common mistake 2 | # of accidentally logging or printing passwords or other secrets. 3 | class Secret 4 | # Initialize a new secret with a given value. 5 | # 6 | # value - anything you want to keep secret from loggers, etc. 7 | def initialize(value) 8 | @value = value 9 | end # def initialize 10 | 11 | # Emit simply "" when printed or logged. 12 | def to_s 13 | return "" 14 | end # def to_s 15 | 16 | alias_method :inspect, :to_s 17 | 18 | # Get the secret value. 19 | def value 20 | return @value 21 | end # def value 22 | end # class Secret 23 | 24 | -------------------------------------------------------------------------------- /dont-log-secrets/ruby/lib/secret2.rb: -------------------------------------------------------------------------------- 1 | # A class for holding a secret. The main goal is to prevent the common mistake 2 | # of accidentally logging or printing passwords or other secrets. 3 | class Secret2 4 | # Initialize a new secret with a given value. 5 | # 6 | # value - anything you want to keep secret from loggers, etc. 7 | def initialize(secret_value) 8 | 9 | # Redefine the 'value' method on this instance. This exposes no instance 10 | # variables to be accidentally leaked by things like awesome_print, etc. 11 | # This makes any #value call return the secret value. 12 | (class << self; self; end).class_eval do 13 | define_method(:value) { secret_value } 14 | end 15 | end # def initialize 16 | 17 | # Emit simply "" when printed or logged. 18 | def to_s 19 | return "" 20 | end # def to_s 21 | 22 | alias_method :inspect, :to_s 23 | 24 | # Get the secret value. 25 | def value 26 | # Nothing, this will be filled in by Secret.new 27 | # But we'll still document this so rdoc/yard know the method exists. 28 | end # def value 29 | end # class Secret2 30 | 31 | -------------------------------------------------------------------------------- /error-reporting/ruby/README.md: -------------------------------------------------------------------------------- 1 | # Error Reporting 2 | 3 | Errors in software happen for many reasons. Loosely, the following reasons: 4 | 5 | * buggy code (invalid state) 6 | * environmental problems (broken components) 7 | * bad user input (invalid configuration or other input) 8 | 9 | For most command-line tools and code libraries, any error is likely to give you 10 | the same result: a confusing message and a big stack trace. 11 | 12 | The error to report depends usually on the intended audience, which I'll 13 | classify roughly here: 14 | 15 | * developer: buggy code. 16 | * admin: environmental problems. 17 | * user: invalid input or configuration 18 | 19 | Having those audiences defined, I think the following is true - each wants 20 | exactly as much data, but no more, to identify and resolve the problem. 21 | Preferrably presented in a way that is easily consumable. 22 | 23 | * developer: structured dump of state (stack, inputs, etC) 24 | * admin: identifying the failing component 25 | * user: identify the problem in a human-readable way, recommend a fix. 26 | 27 | TODO(sissel): implement exception-based errors for each type of audience. 28 | TODO(sissel): getrlimit CORE to see if we should dump stack/state? 29 | -------------------------------------------------------------------------------- /interval-execution/ruby/README.md: -------------------------------------------------------------------------------- 1 | # Interval Execution 2 | 3 | Goal: Run something every N seconds. 4 | 5 | This is common for heartbeats, metric emission, etc. 6 | 7 | Why is it important? I had 50 nodes heartbeating once per second, except I was using the 'implementation 2' method below, and it turned out I was only receiving 49.5 heartbeats per second. Here's a graph of before and after. Before was using implementation 2, after was using implementation 3. 8 | 9 | ![before and after](https://github.com/jordansissel/software-patterns/raw/master/interval-execution/ruby/graph.png) 10 | 11 | The horizontal line in the graph is at 50, see the improvement? :) 12 | 13 | ## Implementations 14 | 15 | ### Naive implementation 16 | 17 | Code: [interval1.rb](https://github.com/jordansissel/software-patterns/blob/master/interval-execution/ruby/examples/interval1.rb) 18 | 19 | This implementation unfortunately really says "After 'code' is done, sleep N 20 | seconds" which is not what we want. You get clock skew: 21 | 22 | ```ruby 23 | % ruby run.rb 1 24 | {:duration=>1.200278292, :skew=>0.1002166980000001, :count=>1, :avgskew=>0.1002166980000001} 25 | {:duration=>1.100280308, :skew=>0.20051446100000003, :count=>2, :avgskew=>0.10025723050000002} 26 | {:duration=>1.100246679, :skew=>0.3007792509999998, :count=>3, :avgskew=>0.10025975033333327} 27 | {:duration=>1.100247607, :skew=>0.4010443800000001, :count=>4, :avgskew=>0.10026109500000002} 28 | 29 | ... 30 | 31 | {:duration=>1.100216171, :skew=>2.706705176, :count=>27, :avgskew=>0.10024833985185184} 32 | {:duration=>1.100220347, :skew=>2.806943812, :count=>28, :avgskew=>0.10024799328571429} 33 | {:duration=>1.100225218, :skew=>2.9071880169999993, :count=>29, :avgskew=>0.1002478626551724} 34 | {:duration=>1.10022926, :skew=>3.007439372999997, :count=>30, :avgskew=>0.1002479790999999} 35 | ``` 36 | 37 | The above gives us an average skew of 0.1002 seconds per iteration, that's 38 | quite a bit. After 30 seconds, we're behind the real time clock by almost 3 39 | seconds. 40 | 41 | Notice the duration of about 1.1 seconds. This is because 'run.rb' is running 42 | some code that takes 0.1 seconds to complete, so this naive implementation 43 | actually invokes the block every 1.1 seconds! Oops! 44 | 45 | ### Better implementation 46 | 47 | Code: [interval2.rb](https://github.com/jordansissel/software-patterns/blob/master/interval-execution/ruby/examples/interval2.rb) 48 | 49 | This implementation tracks the duration of the block call and tries to 50 | compensate by sleeping less. It sleeps the interval time minus the block call 51 | duration. 52 | 53 | ``` 54 | % ruby run.rb 2 55 | {:duration=>1.100209475, :skew=>0.0001255790000000978, :count=>1, :avgskew=>0.0001255790000000978} 56 | {:duration=>1.000071029, :skew=>0.00021426700000004573, :count=>2, :avgskew=>0.00010713350000002286} 57 | {:duration=>1.000075607, :skew=>0.0003079299999999563, :count=>3, :avgskew=>0.00010264333333331876} 58 | {:duration=>1.000072835, :skew=>0.00039882999999996116, :count=>4, :avgskew=>9.970749999999029e-05} 59 | 60 | ... 61 | 62 | {:duration=>1.000097278, :skew=>0.003274076000000292, :count=>27, :avgskew=>0.0001212620740740849} 63 | {:duration=>1.000099535, :skew=>0.003407560999999504, :count=>28, :avgskew=>0.00012169860714283942} 64 | {:duration=>1.000098096, :skew=>0.0035369729999992217, :count=>29, :avgskew=>0.00012196458620686971} 65 | {:duration=>1.000101167, :skew=>0.003671940000000262, :count=>30, :avgskew=>0.00012239800000000874} 66 | ``` 67 | 68 | Skew isn't so bad here. Average skew per iteration is only 0.000122 seconds 69 | (122 microseconds). After only 30 iterations, we're behind by 0.0037 seconds 70 | (3.6ms). However, this is only over 30 iterations. What happens over thousands 71 | of iterations? Lots of skew! 72 | 73 | ### Best? Implementation 74 | 75 | Code: [interval3.rb](https://github.com/jordansissel/software-patterns/blob/master/interval-execution/ruby/examples/interval3.rb) 76 | 77 | This impementation ignores the execution time of the block and keeps 78 | incrementing the target clock by the given interval based on the start time. 79 | This assures that even if we do skew, we will correct for that skew, and 80 | execute at time T[0], T[n], T[2n], etc. 81 | 82 | ``` 83 | % ruby run.rb 3 84 | {:duration=>1.100340615, :skew=>0.0002219629999999917, :count=>1, :avgskew=>0.0002219629999999917} 85 | {:duration=>0.99990078, :skew=>0.00015617599999995235, :count=>2, :avgskew=>7.808799999997618e-05} 86 | {:duration=>0.999961995, :skew=>0.00015248100000020415, :count=>3, :avgskew=>5.082700000006805e-05} 87 | {:duration=>0.999969992, :skew=>0.0001573309999995942, :count=>4, :avgskew=>3.933274999989855e-05} 88 | 89 | ... 90 | 91 | {:duration=>0.999966158, :skew=>0.00014336899999989328, :count=>27, :avgskew=>5.30996296295901e-06} 92 | {:duration=>0.999972856, :skew=>0.00015245199999824877, :count=>28, :avgskew=>5.444714285651742e-06} 93 | {:duration=>0.999936856, :skew=>0.00012359400000150345, :count=>29, :avgskew=>4.26186206901736e-06} 94 | {:duration=>0.99998492, :skew=>0.00014320100000020375, :count=>30, :avgskew=>4.773366666673458e-06} 95 | ``` 96 | 97 | This is the first example where the 'skew' value isn't always increasing. In 98 | fact, it goes down sometimes. The total skew varies up and down around 0.00014 99 | seconds. 100 | 101 | This is good because now we're actually maintaining our "every N seconds, run 102 | this thing" that follows the clock and ignores execution time. 103 | 104 | This is not necessarily the best implementation, but it comes close. The next 105 | problem to focus on, possibly, is what correction should be made if the 106 | execution time is longer than the interval time; for example, if your interval 107 | is 1 second, but the execution takes 2.5 seconds, what do you do? 108 | 109 | The above implementation solves this by resetting the clock and skipping sleep 110 | for the next turn. 111 | 112 | ## Conclusion 113 | 114 | Minimizing skew on interval executions is important for things like heartbeat 115 | and intervals in general. 116 | 117 | This is especially important on multitenant systems because your time 118 | constraints aren't as sharp. Here's what implementation #2 looks like on 119 | Heroku/EC2: 120 | 121 | ``` 122 | {:duration=>1.111534059, :skew=>0.009623941999999941, :count=>1, :avgskew=>0.009623941999999941} 123 | {:duration=>1.009825969, :skew=>0.01945768300000017, :count=>2, :avgskew=>0.009728841500000085} 124 | ... 125 | {:duration=>1.00997368, :skew=>0.2896056629999997, :count=>29, :avgskew=>0.009986402172413781} 126 | {:duration=>1.010086027, :skew=>0.2996890480000012, :count=>30, :avgskew=>0.009989634933333373} 127 | ``` 128 | 129 | The above skews by 10ms per iteration, That's 100 times worse than my local 130 | 'implementation #2' test that I ran on my laptop. Ouch! That's why it's so 131 | important to have an interval execution implementation that corrects for skew 132 | incurred by execution time and resource starvation. 133 | -------------------------------------------------------------------------------- /interval-execution/ruby/examples/interval1.rb: -------------------------------------------------------------------------------- 1 | # This is not the best implemetation. This is the most naive implementation, I 2 | # think, and is certainly the first thing that comes to my mind when I consider 3 | # implementing an interval execution method. 4 | # 5 | # Example showing clock skew: 6 | # 7 | # interval1(1) { puts Time.now.to_f; sleep 0.1 } 8 | def interval1(time, &block) 9 | while true 10 | # The 'block.call' obviously takes some time, and this implementation 11 | # ignores that time consumed! :( 12 | block.call 13 | sleep(time) 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /interval-execution/ruby/examples/interval2.rb: -------------------------------------------------------------------------------- 1 | # This implementation tries to solve the problem identified in 'interval1' 2 | # that is, the 'block.call' can consume time that skews the clock. 3 | # 4 | # Example: 5 | # 6 | # interval2(1) { puts Time.now.to_f; sleep 0.1 } 7 | def interval2(time, &block) 8 | while true 9 | # Keep track of how long 'block.call' took. 10 | start = Time.now 11 | block.call 12 | duration = Time.now - start 13 | 14 | # Now sleep 'interval' seconds less the 'duration' of the 'block.call' 15 | # unless the duration was longer than the interval, in which case 16 | # we do not sleep at all. 17 | sleep(time - duration) if duration > 0 18 | end 19 | end 20 | 21 | -------------------------------------------------------------------------------- /interval-execution/ruby/examples/interval3.rb: -------------------------------------------------------------------------------- 1 | # This implementation tries to keep clock more accurately. 2 | # Prior implementations still permitted skew, where as this one 3 | # will attempt to correct for skew. 4 | # 5 | # The execution patterns of this method should be that 6 | # the start time of 'block.call' should always be at time T*interval 7 | def interval3(time, &block) 8 | start = Time.now 9 | while true 10 | block.call 11 | duration = Time.now - start 12 | # Sleep only if the duration was less than the time interval 13 | if duration < time 14 | sleep(time - duration) 15 | start += time 16 | else 17 | # Duration exceeded interval time, reset the clock and do not sleep. 18 | start = Time.now 19 | end 20 | end # loop forever 21 | end # def interval3 22 | -------------------------------------------------------------------------------- /interval-execution/ruby/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordansissel/software-patterns/1e7e7734f2c1886c18ce58a88da8e2fd3552c820/interval-execution/ruby/graph.png -------------------------------------------------------------------------------- /interval-execution/ruby/run.rb: -------------------------------------------------------------------------------- 1 | $: << File.dirname(__FILE__) 2 | require "examples/interval1" 3 | require "examples/interval2" 4 | require "examples/interval3" 5 | 6 | if ARGV.length != 1 or !["1", "2", "3"].include?(ARGV.first) 7 | puts "Usage: #{$0} <1|2|3>" 8 | exit 1 9 | end 10 | 11 | func = "interval#{ARGV[0]}" 12 | 13 | interval = 1 14 | 15 | boot = nil # boot time, will be recorded later. 16 | start = Time.now 17 | count = 0 18 | method(func).call(interval) do 19 | # pretend to do work that takes a while to illustrate skew problems. 20 | sleep 0.1 21 | 22 | # Skip first iteration 23 | if count > 0 24 | duration = Time.now - start 25 | start = Time.now 26 | 27 | skew = ((Time.now - boot) - (interval * count)) 28 | #p :duration => duration, :skew => sprintf("%.6f", skew), :count => count, :avgskew => sprintf("%.6f", skew / count) 29 | p :duration => duration, :skew => skew, :count => count, :avgskew => skew / count 30 | else 31 | boot = Time.now 32 | end 33 | count += 1 34 | end 35 | -------------------------------------------------------------------------------- /randomized-stress-testing/ruby/examples/analyze_number.rb: -------------------------------------------------------------------------------- 1 | require "randomized" 2 | require "rspec/stress_it" 3 | 4 | RSpec.configure do |c| 5 | c.extend RSpec::StressIt 6 | end 7 | 8 | describe "number" do 9 | let(:number) { Randomized.number(0..200) } 10 | analyze_it "should be less than 100", [:number] do 11 | expect(number).to(be < 100) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /randomized-stress-testing/ruby/examples/socket_acceptance_spec.rb: -------------------------------------------------------------------------------- 1 | require "randomized" 2 | require "socket" 3 | require "rspec/stress_it" 4 | 5 | RSpec.configure do |c| 6 | c.extend RSpec::StressIt 7 | end 8 | 9 | class TCPIntegrationTestFactory 10 | def initialize(port) 11 | @listener = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) 12 | @client = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) 13 | @port = port 14 | end 15 | 16 | def teardown 17 | @listener.close unless @listener.closed? 18 | @client.close unless @listener.closed? 19 | end 20 | 21 | def sockaddr 22 | Socket.sockaddr_in(@port, "127.0.0.1") 23 | end 24 | 25 | def setup 26 | @listener.bind(sockaddr) 27 | @listener.listen(5) 28 | end 29 | 30 | def send_and_receive(text) 31 | @client.connect(sockaddr) 32 | server, _ = @listener.accept 33 | 34 | @client.syswrite(text) 35 | @client.close 36 | #expect(client.syswrite(text)).to(be == text.bytesize) 37 | server.read 38 | ensure 39 | @client.close unless @client.closed? 40 | server.close unless server.nil? || server.closed? 41 | end 42 | end 43 | 44 | describe "TCPServer+TCPSocket" do 45 | let(:port) { Randomized.number(1024..65535) } 46 | let(:text) { Randomized.text(1..10000) } 47 | subject { TCPIntegrationTestFactory.new(port) } 48 | 49 | #describe "using before/after and stress_it2" do 50 | #before do 51 | #begin 52 | #subject.setup 53 | #rescue Errno::EADDRINUSE 54 | ## We chose a random port that was already in use, let's skip this test. 55 | #skip("Port #{port} is in use by another process, skipping") 56 | #end 57 | #end 58 | 59 | #after do 60 | #subject.teardown 61 | #end 62 | 63 | #stress_it2 "should send data correctly" do 64 | #received = subject.send_and_receive(text) 65 | #expect(received).to(be == text) 66 | #end 67 | #end 68 | 69 | describe "using stress_it" do 70 | stress_it "should send data correctly" do 71 | begin 72 | subject.setup 73 | rescue Errno::EADDRINUSE 74 | next # Skip port bindings that are in use 75 | end 76 | 77 | begin 78 | received = subject.send_and_receive(text) 79 | expect(received).to(be == text) 80 | ensure 81 | subject.teardown 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /randomized-stress-testing/ruby/examples/socket_analyze_spec.rb: -------------------------------------------------------------------------------- 1 | require "randomized" 2 | require "socket" 3 | require "rspec/stress_it" 4 | 5 | RSpec.configure do |c| 6 | c.extend RSpec::StressIt 7 | end 8 | 9 | describe TCPServer do 10 | subject(:socket) { Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) } 11 | let(:sockaddr) { Socket.sockaddr_in(port, "127.0.0.1") } 12 | after { socket.close } 13 | 14 | context "on a random port" do 15 | let(:port) { Randomized.number(-100000..100000) } 16 | analyze_it "should bind successfully", [:port] do 17 | socket.bind(sockaddr) 18 | expect(socket.local_address.ip_port).to(be == port) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /randomized-stress-testing/ruby/examples/socket_stress_spec.rb: -------------------------------------------------------------------------------- 1 | require "randomized" 2 | require "socket" 3 | require "rspec/stress_it" 4 | 5 | RSpec.configure do |c| 6 | c.extend RSpec::StressIt 7 | end 8 | 9 | describe TCPServer do 10 | subject(:socket) { Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) } 11 | let(:sockaddr) { Socket.sockaddr_in(port, "127.0.0.1") } 12 | let(:ignore_eaddrinuse) do 13 | proc do |m, *args| 14 | begin 15 | m.call(*args) 16 | rescue Errno::EADDRINUSE 17 | # ignore 18 | end 19 | end 20 | end 21 | 22 | after do 23 | socket.close 24 | end 25 | 26 | context "on privileged ports" do 27 | let(:port) { Randomized.number(1..1023) } 28 | stress_it "should raise Errno::EACCESS" do 29 | expect { socket.bind(sockaddr) }.to(raise_error(Errno::EACCES)) 30 | end 31 | end 32 | 33 | context "on unprivileged ports" do 34 | let(:port) { Randomized.number(1025..65535) } 35 | stress_it "should bind on a port" do 36 | # EADDRINUSE is expected since we are picking ports at random 37 | # Let's ignore this specific exception 38 | allow(socket).to(receive(:bind).and_wrap_original(&ignore_eaddrinuse)) 39 | expect { socket.bind(sockaddr) }.to_not(raise_error) 40 | end 41 | end 42 | 43 | context "on port 0" do 44 | let(:port) { 0 } 45 | stress_it "should bind successfully" do 46 | expect { socket.bind(sockaddr) }.to_not(raise_error) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /randomized-stress-testing/ruby/lib/randomized.rb: -------------------------------------------------------------------------------- 1 | # A collection of methods intended for use in randomized testing. 2 | module Randomized 3 | # Generates text with random characters of a given length (or within a length range) 4 | # 5 | # * The length can be a number or a range `x..y`. If a range, it must be ascending (x < y) 6 | # * Negative lengths are not permitted and will raise an ArgumentError 7 | # 8 | # @param length [Fixnum or Range] the length of text to generate 9 | # @return [String] the 10 | def self.text(length) 11 | if length.is_a?(Range) 12 | raise ArgumentError, "Requires ascending range, you gave #{length}." if length.end < length.begin 13 | raise ArgumentError, "A negative length is not permitted, I received range #{length}" if length.begin < 0 14 | 15 | length = integer(length) 16 | else 17 | raise ArgumentError, "A negative length is not permitted, I received #{length}" if length < 0 18 | end 19 | 20 | length.times.collect { character }.join 21 | end # def text 22 | 23 | # Generates a random character (A string of length 1) 24 | # 25 | # @return [String] 26 | def self.character 27 | # TODO(sissel): Add support to generate valid UTF-8. I started reading 28 | # Unicode 7 (http://www.unicode.org/versions/Unicode7.0.0/) and after much 29 | # reading, I realized I wasn't in my house anymore but had somehow lost 30 | # track of time and was alone in a field. Civilization had fallen centuries 31 | # ago. :P 32 | 33 | # Until UTF-8 is supported, just return a random lower ASCII character 34 | integer(32..127).chr 35 | end # def character 36 | 37 | # Return a random integer value within a given range. 38 | # 39 | # @param range [Range] 40 | def self.integer(range) 41 | raise ArgumentError, "Range not given, got #{range.class}: #{range.inspect}" if !range.is_a?(Range) 42 | rand(range) 43 | end # def integer 44 | 45 | # Return a random number within a given range. 46 | # 47 | # @param range [Range] 48 | def self.number(range) 49 | raise ArgumentError, "Range not given, got #{range.class}: #{range.inspect}" if !range.is_a?(Range) 50 | # Range#size returns the number of elements in the range, not the length of the range. 51 | # This makes #size return (abs(range.begin - range.end) + 1), and we want the length, so subtract 1. 52 | rand * (range.size - 1) + range.begin 53 | end # def number 54 | 55 | # Run a block a random number of times. 56 | # 57 | # @param range [Fixnum of Range] same meaning as #integer(range) 58 | def self.iterations(range, &block) 59 | range = 0..range if range.is_a?(Numeric) 60 | if block_given? 61 | integer(range).times(&block) 62 | nil 63 | else 64 | integer(range).times 65 | end 66 | end # def iterations 67 | end 68 | -------------------------------------------------------------------------------- /randomized-stress-testing/ruby/lib/rspec/stress_it.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "rspec/core" 3 | 4 | module RSpec::StressIt 5 | DEFAULT_ITERATIONS = 1..5000 6 | 7 | # Wraps `it` and runs the block many times. Each run has will clear the `let` cache. 8 | # 9 | # The intent of this is to allow randomized testing for fuzzing and stress testing 10 | # of APIs to help find edge cases and weird behavior. 11 | # 12 | # The default number of iterations is randomly selected between 1 and 1000 inclusive 13 | def stress_it(name, options={}, &block) 14 | __iterations = Randomized.iterations(options.delete(:stress_iterations) || DEFAULT_ITERATIONS) 15 | it(name, options) do 16 | # Run the block of an example many times 17 | __iterations.each do |i| 18 | # Run the block within 'it' scope 19 | instance_eval(&block) 20 | 21 | # clear the internal rspec `let` cache this lets us run a test 22 | # repeatedly with fresh `let` evaluations. 23 | # Reference: https://github.com/rspec/rspec-core/blob/5fc29a15b9af9dc1c9815e278caca869c4769767/lib/rspec/core/memoized_helpers.rb#L124-L127 24 | __memoized.clear 25 | end 26 | end # it ... 27 | end # def stress_it 28 | 29 | # Generate a random number of copies of a given example. 30 | # The idea is to take 1 `it` and run it N times to help tease out failures. 31 | # Of course, the teasing requires you have randomized `let` usage, for example: 32 | # 33 | # let(:number) { Randomized.number(0..200) } 34 | # it "should be less than 100" do 35 | # expect(number).to(be < 100) 36 | # end 37 | def stress_it2(name, options={}, &block) 38 | __iterations = Randomized.iterations(options.delete(:stress_iterations) || DEFAULT_ITERATIONS) 39 | __iterations.each do |i| 40 | it(example_name + " [#{i}]", *args) do 41 | instance_eval(&block) 42 | end # it ... 43 | end # .times 44 | end 45 | 46 | # Perform analysis on failure scenarios of a given example 47 | # 48 | # This will run the given example a random number of times and aggregate the 49 | # results. If any failures occur, the spec will fail and a report will be 50 | # given on that test. 51 | # 52 | # Example spec: 53 | # 54 | # let(:number) { Randomized.number(0..200) } 55 | # fuzz "should be less than 100" do 56 | # expect(number).to(be < 100) 57 | # end 58 | # 59 | # Example report: 60 | def analyze_it(name, variables, &block) 61 | it(name) do 62 | results = Hash.new { |h,k| h[k] = [] } 63 | iterations = Randomized.iterations(DEFAULT_ITERATIONS) 64 | iterations.each do |i| 65 | state = Hash[variables.collect { |l| [l, __send__(l)] }] 66 | begin 67 | instance_eval(&block) 68 | results[:success] << [state, nil] 69 | rescue => e 70 | results[e.class] << [state, e] 71 | rescue Exception => e 72 | results[e.class] << [state, e] 73 | end 74 | 75 | # Clear `let` memoizations 76 | __memoized.clear 77 | end 78 | 79 | if results[:success] != iterations 80 | raise Analysis.new(results) 81 | end 82 | end 83 | end 84 | 85 | class Analysis < StandardError 86 | def initialize(results) 87 | @results = results 88 | end 89 | 90 | def total 91 | @results.reduce(0) { |m, (k,v)| m + v.length } 92 | end 93 | 94 | def success_count 95 | if @results.include?(:success) 96 | @results[:success].length 97 | else 98 | 0 99 | end 100 | end 101 | 102 | def percent(count) 103 | return (count + 0.0) / total 104 | end 105 | 106 | def percent_s(count) 107 | return sprintf("%.2f%%", percent(count) * 100) 108 | end 109 | 110 | def to_s 111 | # This method is crazy complex for a formatter. Should refactor this significantly. 112 | report = ["#{percent_s(success_count)} tests successful of #{total} tests"] 113 | if success_count < total 114 | report << "Failure analysis:" 115 | report += @results.sort_by { |k,v| -v.length }.reject { |k,v| k == :success }.collect do |k, v| 116 | sample = v.sample(5).collect { |v| v.first }.join(", ") 117 | [ 118 | " #{percent_s(v.length)} -> [#{v.length}] #{k}", 119 | " Sample exception:", 120 | v.sample(1).first[1].to_s.gsub(/^/, " "), 121 | " Samples causing #{k}:", 122 | *v.sample(5).collect { |state, _exception| " #{state}" } 123 | ] 124 | end.flatten 125 | end 126 | report.join("\n") 127 | end 128 | end 129 | end # module RSpec::StressIt 130 | -------------------------------------------------------------------------------- /randomized-stress-testing/ruby/spec/randomized_spec.rb: -------------------------------------------------------------------------------- 1 | require "randomized" 2 | require "rspec/stress_it" 3 | 4 | RSpec.configure do |c| 5 | c.extend RSpec::StressIt 6 | end 7 | 8 | describe Randomized do 9 | describe "#text" do 10 | context "with no arguments" do 11 | it "should raise ArgumentError" do 12 | expect { subject.text }.to(raise_error(ArgumentError)) 13 | end 14 | end 15 | 16 | context "with 1 length argument" do 17 | subject { described_class.text(length) } 18 | 19 | context "that is positive" do 20 | let(:length) { rand(1..1000) } 21 | stress_it "should give a string with that length" do 22 | expect(subject).to(be_a(String)) 23 | expect(subject.length).to(eq(length)) 24 | end 25 | end 26 | 27 | context "that is negative" do 28 | let(:length) { -1 * rand(1..1000) } 29 | stress_it "should raise ArgumentError" do 30 | expect { subject }.to(raise_error(ArgumentError)) 31 | end 32 | end 33 | end 34 | 35 | context "with 1 range argument" do 36 | let(:start) { rand(1..1000) } 37 | let(:length) { rand(1..1000) } 38 | subject { described_class.text(range) } 39 | 40 | context "that is ascending" do 41 | let(:range) { start .. (start + length) } 42 | stress_it "should give a string within that length range" do 43 | expect(subject).to(be_a(String)) 44 | expect(range).to(include(subject.length)) 45 | end 46 | end 47 | 48 | context "that is descending" do 49 | let(:range) { start .. (start - length) } 50 | stress_it "should raise ArgumentError" do 51 | expect { subject }.to(raise_error(ArgumentError)) 52 | end 53 | end 54 | end 55 | end 56 | 57 | describe "#character" do 58 | subject { described_class.character } 59 | stress_it "returns a string of length 1" do 60 | expect(subject.length).to(be == 1) 61 | end 62 | end 63 | 64 | shared_examples_for "random numbers within a range" do 65 | let(:start) { Randomized.integer(-100000 .. 100000) } 66 | let(:length) { Randomized.integer(1 .. 100000) } 67 | let(:range) { start .. (start + length) } 68 | 69 | stress_it "should be a Numeric" do 70 | expect(subject).to(be_a(Numeric)) 71 | end 72 | 73 | stress_it "should be within the bounds of the given range" do 74 | expect(range).to(include(subject)) 75 | end 76 | end 77 | 78 | describe "#integer" do 79 | it_behaves_like "random numbers within a range" do 80 | subject { Randomized.integer(range) } 81 | stress_it "is a Fixnum" do 82 | expect(subject).to(be_a(Fixnum)) 83 | end 84 | end 85 | end 86 | describe "#number" do 87 | it_behaves_like "random numbers within a range" do 88 | subject { Randomized.number(range) } 89 | 90 | stress_it "is a Float" do 91 | expect(subject).to(be_a(Float)) 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /resource-pool/ruby/README.md: -------------------------------------------------------------------------------- 1 | # Pooled resources 2 | 3 | Frequently, you will have a dynamic set of resources that can answer requests. 4 | 5 | For example: 6 | 7 | * maintaining a pool of HTTP connections (with keep-alive) open to backend 8 | servers to reduce tcp/ssl handshake latency 9 | * keeping a pool of database connections to many read-only slaves 10 | 11 | ## Implementation options 12 | 13 | Worker and Thread pool implementations are quite common in many languages, and 14 | may suit your needs here. However, I'm looking specifically to solve a more 15 | general "resource" pool. 16 | 17 | The work flow I wanted here was: 18 | 19 | 1. Get a resource from the pool 20 | * if none available, create a new one or block until one is available 21 | 2. Do something with this resource 22 | 3. Release the resource back to the pool 23 | 4. Remove bad resources (a database died, for example) 24 | 25 | With this flow, we can implement thread pools, workers, etc, but also implement 26 | connection pooling, etc. 27 | 28 | ## Example Code 29 | 30 | * See [`example.rb`](https://github.com/jordansissel/software-patterns/blob/master/resource-pool/ruby/example.rb) for the sample user code which maintaines a pool of database connections. 31 | * See [`lib/pool.rb'](https://github.com/jordansissel/software-patterns/blob/master/resource-pool/ruby/lib/pool.rb) for the 'pool' implementation 32 | 33 | ## Specific Examples 34 | 35 | ### Active pool of healthy database connections 36 | 37 | ``` 38 | % ruby example.rb 39 | Database failed (#), but will try another. Error was: Sequel::DatabaseConnectionError 40 | 14363500: In slow query (#) 41 | 14392280: In slow query (#) 42 | 14407200: In slow query (#) 43 | Database failed (#), but will try another. Error was: Sequel::DatabaseConnectionError 44 | 14456720: In slow query (#) 45 | 14473340: In slow query (#) 46 | => Pool is full and nothing available. Waiting for a release... 47 | 14363500: In slow query (#) 48 | => Pool is full and nothing available. Waiting for a release... 49 | 14392280: In slow query (#) 50 | => Pool is full and nothing available. Waiting for a release... 51 | 14407200: In slow query (#) 52 | => Pool is full and nothing available. Waiting for a release... 53 | 14456720: In slow query (#) 54 | => Pool is full and nothing available. Waiting for a release... 55 | 14473340: In slow query (#) 56 | => Pool is full and nothing available. Waiting for a release... 57 | 14363500: In slow query (#) 58 | => Pool is full and nothing available. Waiting for a release... 59 | 14392280: In slow query (#) 60 | => Pool is full and nothing available. Waiting for a release... 61 | 14407200: In slow query (#) 62 | => Pool is full and nothing available. Waiting for a release... 63 | 14456720: In slow query (#) 64 | => Pool is full and nothing available. Waiting for a release... 65 | 14473340: In slow query (#) 66 | ``` 67 | -------------------------------------------------------------------------------- /resource-pool/ruby/example.rb: -------------------------------------------------------------------------------- 1 | require "./lib/pool" 2 | 3 | require "sequel" 4 | Thread.abort_on_exception = true 5 | 6 | def slowquery(db) 7 | # Simulates a slow query, just sleeps, really. 8 | puts "#{db.object_id}: In slow query (#{db})" 9 | sleep 1 10 | end 11 | 12 | # limit maximum of number resources in the pool. 13 | # You can omit the size to make an infinite-sized pool. 14 | pool = Pool.new(5) 15 | 16 | # Use multiple database resources in the pool. 17 | # In this example, the 'some/bad/path' one should fail and help demonstrate 18 | # the removal of a 'bad' resource as well as retries on resource failures. 19 | dburls = [ "sqlite://test1.db", "sqlite://test2.db", "sqlite://some/bad/path" ] 20 | 21 | 15.times do 22 | # Get a database connection and make sure it works before continuing. 23 | begin 24 | # Fetch a resource from the pool. If nothing is available and the pool is 25 | # not full, pick a random database URL and and use it. Otherwise, this fetch 26 | # call will block until the pool has an available resource. 27 | db = pool.fetch { Sequel.connect(dburls.shuffle.first) } 28 | 29 | # Test the connection to verify it still works (see Sequel::Database#test_connection) 30 | # If this fails, we'll catch the exception and remove this known-broken 31 | # database connection from the pool. 32 | db.test_connection 33 | rescue Sequel::DatabaseConnectionError => e 34 | puts "Database failed (#{db.inspect}), but will try another. Error was: #{e.class}" 35 | #p :busy => pool.instance_eval { @busy }, :available => pool.instance_eval { @available } 36 | pool.remove(db) 37 | 38 | # Now retry the fetch, which possibly will create a new database connection to replace 39 | # the broken one we just removed. 40 | retry 41 | end 42 | 43 | # Run a slow query in a separate thread. 44 | Thread.new(db) do |db| 45 | slowquery(db) 46 | 47 | # Notify the pool that this resource is free again. 48 | pool.release(db) 49 | end 50 | 51 | # Sleep a bit, pretending to do other work in the main thread here. 52 | sleep 0.1 53 | end 54 | -------------------------------------------------------------------------------- /resource-pool/ruby/lib/pool.rb: -------------------------------------------------------------------------------- 1 | require "thread" 2 | 3 | # Public: A thread-safe, generic resource pool. 4 | # 5 | # This class is agnostic as to the resources in the pool. You can put 6 | # database connections, sockets, threads, etc. It's up to you! 7 | # 8 | # Examples: 9 | # 10 | # pool = Pool.new 11 | # pool.add(Sequel.connect("postgres://pg-readonly-1/prod")) 12 | # pool.add(Sequel.connect("postgres://pg-readonly-2/prod")) 13 | # pool.add(Sequel.connect("postgres://pg-readonly-3/prod")) 14 | # 15 | # pool.fetch # => Returns one of the Sequel::Database values from the pool 16 | class Pool 17 | 18 | class Error < StandardError; end 19 | 20 | # An error indicating a given resource is busy. 21 | class ResourceBusy < Error; end 22 | 23 | # An error indicating a given resource is not found. 24 | class NotFound < Error; end 25 | 26 | # You performed an invalid action. 27 | class InvalidAction < Error; end 28 | 29 | # Default all methods to private. See the bottom of the class definition for 30 | # public method declarations. 31 | private 32 | 33 | # Public: initialize a new pool. 34 | # 35 | # max_size - if specified, limits the number of resources allowed in the pool. 36 | def initialize(max_size=nil) 37 | # Available resources 38 | @available = Hash.new 39 | # Busy resources 40 | @busy = Hash.new 41 | 42 | # The pool lock 43 | @lock = Mutex.new 44 | 45 | # Locks for blocking {#fetch} calls if the pool is full. 46 | @full_lock = Mutex.new 47 | @full_cv = ConditionVariable.new 48 | 49 | # Maximum size of this pool. 50 | @max_size = max_size 51 | end # def initialize 52 | 53 | # Private: Is this pool size-limited? 54 | # 55 | # Returns true if this pool was created with a max_size. False, otherwise. 56 | def sized? 57 | return !@max_size.nil? 58 | end # def sized? 59 | 60 | # Private: Is this pool full? 61 | # 62 | # Returns true if the pool is sized and the count of resources is at maximum. 63 | def full? 64 | return sized? && (count == @max_size) 65 | end # def full? 66 | 67 | # Public: the count of resources in the pool 68 | # 69 | # Returns the count of resources in the pool. 70 | def count 71 | return (@busy.size + @available.size) 72 | end # def count 73 | 74 | # Public: Add a new resource to this pool. 75 | # 76 | # The resource, once added, is assumed to be available for use. 77 | # That means once you add it, you must not use it unless you receive it from 78 | # {Pool#fetch} 79 | # 80 | # resource - the object resource to add to the pool. 81 | # 82 | # Returns nothing 83 | def add(resource) 84 | @lock.synchronize do 85 | @available[resource.object_id] = resource 86 | end 87 | return nil 88 | end # def add 89 | 90 | # Public: Fetch an available resource. 91 | # 92 | # If no resource is available, and the pool is not full, the 93 | # default_value_block will be called and the return value of it used as the 94 | # resource. 95 | # 96 | # If no resource is availabe, and the pool is full, this call will block 97 | # until a resource is available. 98 | # 99 | # Returns a resource ready to be used. 100 | def fetch(&default_value_block) 101 | @lock.synchronize do 102 | object_id, resource = @available.shift 103 | if !resource.nil? 104 | @busy[resource.object_id] = resource 105 | return resource 106 | end 107 | end 108 | 109 | @full_lock.synchronize do 110 | if full? 111 | # This should really use a logger. 112 | puts "=> Pool is full and nothing available. Waiting for a release..." 113 | @full_cv.wait(@full_lock) 114 | return fetch(&default_value_block) 115 | end 116 | end 117 | 118 | # TODO(sissel): If no block is given, we should block until a resource is 119 | # available. 120 | 121 | # If we get here, no resource is available and the pool is not full. 122 | resource = default_value_block.call 123 | # Only add the resource if the default_value_block returned one. 124 | if !resource.nil? 125 | add(resource) 126 | return fetch 127 | end 128 | end # def fetch 129 | 130 | # Public: Remove a resource from the pool. 131 | # 132 | # This is useful if the resource is no longer useful. For example, if it is 133 | # a database connection and that connection has failed. 134 | # 135 | # This resource *MUST* be available and not busy. 136 | # 137 | # Raises Pool::NotFound if no such resource is found. 138 | # Raises Pool::ResourceBusy if the resource is found but in use. 139 | def remove(resource) 140 | # Find the object by object_id 141 | #p [:internal, :busy => @busy, :available => @available] 142 | @lock.synchronize do 143 | if available?(resource) 144 | raise InvalidAction, "This resource must be busy for you to remove it (ie; it must be fetched from the pool)" 145 | end 146 | @busy.delete(resource.object_id) 147 | end 148 | end # def remove 149 | 150 | # Private: Verify this resource is in the pool. 151 | # 152 | # You *MUST* call this method only when you are holding @lock. 153 | # 154 | # Returns :available if it is available, :busy if busy, false if not in the pool. 155 | def include?(resource) 156 | if @available.include?(resource.object_id) 157 | return :available 158 | elsif @busy.include?(resource.object_id) 159 | return :busy 160 | else 161 | return false 162 | end 163 | end # def include? 164 | 165 | # Private: Is this resource available? 166 | # You *MUST* call this method only when you are holding @lock. 167 | # 168 | # Returns true if this resource is available in the pool. 169 | # Raises NotFound if the resource given is not in the pool at all. 170 | def available?(resource) 171 | case include?(resource) 172 | when :available; return true 173 | when :busy; return false 174 | else; raise NotFound, "No resource, #{resource.inspect}, found in pool" 175 | end 176 | end # def avilable? 177 | 178 | # Private: Is this resource busy? 179 | # 180 | # You *MUST* call this method only when you are holding @lock. 181 | # 182 | # Returns true if this resource is busy. 183 | # Raises NotFound if the resource given is not in the pool at all. 184 | def busy?(resource) 185 | return !available?(resource) 186 | end # def busy? 187 | 188 | # Public: Release this resource back to the pool. 189 | # 190 | # After you finish using a resource you received with {#fetch}, you must 191 | # release it back to the pool using this method. 192 | # 193 | # Alternately, you can {#remove} it if you want to remove it from the pool 194 | # instead of releasing it. 195 | def release(resource) 196 | @lock.synchronize do 197 | if !include?(resource) 198 | raise NotFound, "No resource, #{resource.inspect}, found in pool" 199 | end 200 | 201 | # Release is a no-op if this resource is already available. 202 | #return if available?(resource) 203 | @busy.delete(resource.object_id) 204 | @available[resource.object_id] = resource 205 | 206 | # Notify any threads waiting on a resource from the pool. 207 | @full_lock.synchronize { @full_cv.signal } 208 | end 209 | end # def release 210 | 211 | public(:add, :remove, :fetch, :release, :sized?, :count) 212 | end # def Pool 213 | -------------------------------------------------------------------------------- /retry-on-failure/ruby/README.md: -------------------------------------------------------------------------------- 1 | # Retry on Failure 2 | 3 | Lots of functions fail nondeterministically; that is, they fail for reasons unrelated to the input. 4 | 5 | For example, a HTTP GET may fail because the server is down temporarily. 6 | 7 | ## Implementation options 8 | 9 | Most errors in Ruby appear as exceptions, thus can be handled with 'rescue'. A 10 | begin/rescue block supports 'retry' which causes the begin block to be 11 | re-executed, kind of like a loop: 12 | 13 | ```ruby 14 | tries = 10 15 | begin 16 | # some code that might fail 17 | rescue SomeException 18 | tries -= 1 19 | retry if tries > 0 # restarts the 'begin' block 20 | end 21 | ``` 22 | 23 | You can also use the in-line rescue to retry forever, which is pretty awesome 24 | if you don't need to log the failure: 25 | 26 | ```ruby 27 | # Retry this http fetch until it succeeds 28 | response = Net::HTTP.get_response("google.com", "/") rescue retry 29 | ``` 30 | 31 | However, when I started generalizing this solution, I found that using 32 | begin+rescue and an enumerable was a more flexible solution, like so: 33 | 34 | ```ruby 35 | 10.times.each do 36 | begin 37 | # some code 38 | break # if we get here, success! 39 | rescue SomeException => e 40 | last_exception = e 41 | end 42 | raise last_exception 43 | end 44 | ``` 45 | 46 | ## Example Code 47 | 48 | * See [`example.rb`](https://github.com/jordansissel/software-patterns/blob/master/retry-on-failure/ruby/example.rb) for the sample user code. 49 | * See [`example-sequel.rb`](https://github.com/jordansissel/software-patterns/blob/master/retry-on-failure/ruby/example-sequel.rb) 50 | for the sample Sequel code trying connections to a set of database hosts. 51 | * See [`lib/try.rb'](https://github.com/jordansissel/software-patterns/blob/master/retry-on-failure/ruby/lib/try.rb) for the 'try' implementation 52 | 53 | This 'try' implementation uses an enumerable instead of Numeric value to clue 54 | the number of attempts. This flows well: 55 | 56 | ## Specific Examples 57 | 58 | ### Try a few times: 59 | ``` 60 | try(5.times) do 61 | # some code 62 | end 63 | ``` 64 | 65 | ### Pass the enumerable value in: 66 | ``` 67 | try(5.times) do |iteration| 68 | puts "On try: #{iteration}" # prints 'On try: 1', etc 69 | # some code 70 | end 71 | ``` 72 | ### Try forever until success 73 | 74 | ``` 75 | try do 76 | # code ... 77 | end 78 | ``` 79 | 80 | ## Example runs 81 | 82 | ### Fail forever 83 | 84 | ``` 85 | % ruby -r ./lib/try -e 'try { raise "Uh oh" } ' 86 | Failed (Uh oh). Retrying in 0.01 seconds... 87 | Failed (Uh oh). Retrying in 0.02 seconds... 88 | Failed (Uh oh). Retrying in 0.04 seconds... 89 | Failed (Uh oh). Retrying in 0.08 seconds... 90 | Failed (Uh oh). Retrying in 0.16 seconds... 91 | (this continues forever) 92 | ``` 93 | 94 | 95 | ### Ran out of tries: 96 | 97 | ``` 98 | % ruby example.rb http://www.google.com/ 99 | Failed (Simulated random failure). Retrying in 0.01 seconds... 100 | Failed (Simulated random failure). Retrying in 0.02 seconds... 101 | Failed (Simulated random failure). Retrying in 0.04 seconds... 102 | Failed (Simulated random failure). Retrying in 0.08 seconds... 103 | Failed (Simulated random failure). Retrying in 0.16 seconds... 104 | /home/jls/projects/software-patterns/retry-on-failure/ruby/lib/try.rb:47:in `try': Simulated random failure (HTTP::Error) 105 | from example.rb:14:in `
' 106 | ``` 107 | 108 | ### First-time success: 109 | 110 | ``` 111 | % ruby example.rb http://www.google.com/ 112 | Response status: 200 113 | ``` 114 | 115 | ### First fail, second success: 116 | 117 | ``` 118 | % ruby example.rb http://www.google.com/ 119 | Failed (Simulated random failure). Retrying in 0.01 seconds... 120 | Response status: 200 121 | ``` 122 | 123 | ### Try multiple database hosts 124 | 125 | This one tries a shuffled list of database hosts. 126 | 127 | ``` 128 | % ruby example-sequel.rb 129 | Failed (PG::Error: could not translate host name "pg-replica-b" to address: Name or service not known 130 | ). Retrying in 0.01 seconds... 131 | Failed (PG::Error: could not translate host name "pg-replica-c" to address: Name or service not known 132 | ). Retrying in 0.02 seconds... 133 | Failed (PG::Error: could not translate host name "pg-replica-a" to address: Name or service not known 134 | ). Retrying in 0.04 seconds... 135 | /home/jls/projects/software-patterns/retry-on-failure/ruby/lib/try.rb:75:in `try': PG::Error: could not translate host name "pg-replica-a" to address: Name or service not known (Sequel::DatabaseConnectionError) 136 | from example-sequel.rb:9:in `
' 137 | ``` 138 | -------------------------------------------------------------------------------- /retry-on-failure/ruby/example-sequel.rb: -------------------------------------------------------------------------------- 1 | require "sequel" 2 | require "./lib/try" 3 | 4 | pg_hosts = ["pg-replica-a", "pg-replica-b", "pg-replica-c"] 5 | 6 | # Try connecting to any database host. 7 | # Also, shuffle the list before attempting so that we get a more balanced 8 | # connection load. 9 | database = try(pg_hosts.shuffle) do |host| 10 | db = Sequel.connect("postgres://#{host}/example") 11 | # Sequel::Database#test_connection returns true if successful. 12 | # So return the db if we succeed, otherwise raises an error 13 | # in which case we will retry. 14 | db if db.test_connection 15 | end 16 | -------------------------------------------------------------------------------- /retry-on-failure/ruby/example.rb: -------------------------------------------------------------------------------- 1 | # A simple example that does a HTTP fetch and retries a few times on failure. 2 | require "./lib/http" # implements the HTTP.get method 3 | require "./lib/try" # actually implements the begin/rescue/retry code. 4 | 5 | if ARGV.length != 1 6 | puts "Usage: #{$0} " 7 | puts "Example: #{$0} http://www.google.com/" 8 | exit 1 9 | end 10 | 11 | url = ARGV[0] 12 | 13 | response = try(5.times) do 14 | # Simulate failure 70% of the time. 15 | raise HTTP::Error, "Simulated random failure" if rand < 0.70 16 | HTTP.get(url) 17 | end 18 | 19 | puts "Response status: #{response.status}" 20 | 21 | -------------------------------------------------------------------------------- /retry-on-failure/ruby/lib/http.rb: -------------------------------------------------------------------------------- 1 | require "ftw" # gem install 'ftw' 2 | 3 | module HTTP 4 | class Error < StandardError; end 5 | def self.get(url) 6 | response = agent.get!(url) 7 | 8 | # Raise an exception on server errors. 9 | if (500..599).include?(response.status) 10 | raise Error, "Status code #{response.status} from GET #{url}" 11 | end 12 | 13 | return response 14 | end # def self.get 15 | 16 | def self.agent 17 | @agent ||= FTW::Agent.new 18 | end # def self.agent 19 | end # module HTTP 20 | -------------------------------------------------------------------------------- /retry-on-failure/ruby/lib/try.rb: -------------------------------------------------------------------------------- 1 | # Public: try a block of code until either it succeeds or we give up. 2 | # 3 | # enumerable - an Enumerable or omitted, #each is invoked and is tried that 4 | # number of times. If this value is omitted or nil, we will try until 5 | # success with no limit on the number of tries. 6 | # 7 | # Returns the return value of the block once the block succeeds. 8 | # Raises the last seen exception if we run out of tries. 9 | # 10 | # Examples 11 | # 12 | # # Try 10 times 13 | # response = try(10.times) { Net::HTTP.get_response("google.com", "/") } 14 | # 15 | # # Try many times, yielding the value of the enumeration to the block. 16 | # # This allows you to try different inputs. 17 | # response = try([0, 2, 4, 6]) { |val| 50 / val } 18 | # 19 | # Output: 20 | # Failed (divided by 0). Retrying in 0.01 seconds... 21 | # => 25 22 | # 23 | # 24 | # # Try forever 25 | # return_value = try { ... } 26 | # 27 | # The above will try fetching http://google.com/ at most 10 times, breaking 28 | # after the first non-exception return. 29 | def try(enumerable=nil, &block) 30 | if block.arity == 0 31 | # If the block takes no arguments, give none 32 | procedure = lambda { |val| block.call } 33 | else 34 | # Otherwise, pass the current 'enumerable' value to the block. 35 | procedure = lambda { |val| block.call(val) } 36 | end 37 | 38 | last_exception = nil 39 | 40 | # Retry after a sleep to be nice. 41 | backoff = 0.01 42 | backoff_max = 2.0 43 | 44 | # Try forever if enumerable is nil. 45 | if enumerable.nil? 46 | enumerable = Enumerator.new do |y| 47 | a = 0 48 | while true 49 | a += 1 50 | y << a 51 | end 52 | end 53 | end 54 | 55 | # When 'enumerable' runs out of things, if we still haven't succeeded, we'll 56 | # reraise 57 | enumerable.each do |val| 58 | begin 59 | # If the 'procedure' (the block, really) succeeds, we'll break 60 | # and return the return value of the block. Win! 61 | return procedure.call(val) 62 | rescue => e 63 | puts "Failed (#{e}). Retrying in #{backoff} seconds..." 64 | last_exception = e 65 | 66 | # Exponential backoff 67 | sleep(backoff) 68 | backoff = [backoff * 2, backoff_max].min unless backoff == backoff_max 69 | end 70 | end 71 | 72 | # generally make the exception appear from the 'try' method itself, not from 73 | # any deeply nested enumeration/begin/etc 74 | last_exception.set_backtrace(StandardError.new.backtrace) 75 | raise last_exception 76 | end # def try 77 | -------------------------------------------------------------------------------- /supervising-threads/ruby/lib/supervise.rb: -------------------------------------------------------------------------------- 1 | require "thread" 2 | 3 | class Supervisor 4 | def initialize(*args, &block) 5 | @args = args 6 | @block = block 7 | 8 | run 9 | end # def initialize 10 | 11 | def run 12 | while true 13 | task = Task.new(*@args, &@block) 14 | begin 15 | puts :result => task.wait 16 | rescue => e 17 | puts e 18 | puts e.backtrace 19 | end 20 | end 21 | end # def run 22 | 23 | class Task 24 | def initialize(*args, &block) 25 | # A queue to receive the result of the block 26 | @queue = Queue.new 27 | @thread = Thread.new(@queue, *args) do |queue, *args| 28 | begin 29 | result = block.call(*args) 30 | queue << [:return, result] 31 | rescue => e 32 | queue << [:exception, e] 33 | end 34 | end # thread 35 | end # def initialize 36 | 37 | def wait 38 | @thread.join 39 | reason, result = @queue.pop 40 | 41 | if reason == :exception 42 | #raise StandardError.new(result) 43 | raise result 44 | else 45 | return result 46 | end 47 | end # def wait 48 | end # class Supervisor::Task 49 | end # class Supervisor 50 | 51 | def supervise(&block) 52 | Supervisor.new(&block) 53 | end # def supervise 54 | -------------------------------------------------------------------------------- /supervising-threads/ruby/test.rb: -------------------------------------------------------------------------------- 1 | require "./lib/supervise" 2 | 3 | supervise do 4 | puts "OK" 5 | sleep 0.2 6 | if rand > 0.5 7 | raise "Failure" 8 | else 9 | "Success" 10 | end 11 | end 12 | --------------------------------------------------------------------------------