Singletons, Threads, and Flexibility
In Ruby, we often like very simple APIs, but this often comes at the price of thread safety and code flexibility. I’ve found that if you use a few tricks from the start, you can get the best of it all.
I recently did a project where I tried to use the VCR gem, but it went awry when working in multiple threads. This is a great gem that, like many of my own, falls into the trap of module/class level singleton configuration/execution.
This is approach is characterized by things like extend self
in the top-level module and then having instance variables at that level. This is not to call out VCR specifically. it’s just my most recent example of hundreds of gems that take this overall approach.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
When operating on multiple threads, things get wacky because of this because they are sharing this current_cassette
and writing to the associated file. You end up with recordings on top of each other.
I am inclined (and some say over-inclined) to use singletons to do something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
The most common use case of the module doesn’t change at all because I delegate everything to a default one. You can still do:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
and it will use the default_client.
But this whole scheme now allows my threaded code to do something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
Clearly there is more code, but it is now 8x (or whatever) faster.
One example that I’ve seen done really well in this way is the twitter gem and others that seems to follow that pattern like octokit which I used for hubtime in such a threaded way.
Again, I’m not calling out VCR or anything and I’m sure I’ve trivialized the complexity involved. I would love to put a pull request link to VCR here, but alas, for another time.
If you do this from the beginning, though, it can be a strong win with minimal overhead. It adds multi-threaded capabilities as well as the ability (such as with twitter) to work with two different users in your app without changing anything global.