theme | highlighter | lineNumbers | info | drawings | fonts | aspectRatio | transition | title | mdc | |||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
default |
shiki |
false |
# Threads, callbacks, and execution context in Ruby
When you provide a block to a function in Ruby, do you know when and where that block will be executed? What is safe to do inside the block, and what is dangerous? Let’s take a look at various code examples and understand what dragons are hidden in Ruby dungeons.
|
|
|
4/3 |
slide-left |
Threads, callbacks, and execution context in Ruby |
true |
Hi, I'm Andrey (アンドレイ){class="text-xl"}
-
Back-end engineer at Evil Martians
-
Writing Ruby, Go, and whatever
SQL, Dockerfiles, TypeScript, bash…
-
Love open-source software
Created and maintaining a few Ruby gems
-
Living in Japan for 1 year already
-
Driving a moped
And also a bicycle to get kids to kindergarten
Our base is just 30 minutes walk away from here!
Please come visit us! 1
3.times do |i|
puts "Hello, Osaka RubyKaigi #0#{i}!"
end
It feels like code between do
and end
is in the same flow with surrounding code, right?
WRONG!
Block is separate entity, that is passed to times
method as an argument and got called by it.
greet = proc do |i|
puts "Hello, Osaka RubyKaigi #0#{i}!"
end
greet.call(3)
# also
3.times(&greet)
Illustration from the Ruby under a microscope book (日本語版: Rubyのしくみ)
We often use blocks as callbacks to hook our own behavior for someone's else code.
def some_method(&block)
block.call
# and/or
@callbacks << block
end
some_method do
puts "Hey, I was called!"
end
# which one??? will it be called at all?
- When does the block get executed?
- And where?
- How to understand?
When and where will the block be called?
No way to know! 😭
Well, except reading method documentation and source code.
And memorize, memorize, and memorize.
All Enumerable
methods are sync, and will call provided block during their execution.
3.times do |i|
puts "Hello, Osaka RubyKaigi #0#{i}!"
end
E.g. times
will yield
to the block on every iteration.
Much later.
after_commit do
puts "Hello, Osaka RubyKaigi #03!"
end
ActiveRecord callbacks and also after_commit
from after_commit_everywhere gem will store callback proc in ActiveRecord internals for later.
result = []
work = proc do |arg|
# Can you tell which thread is executing me?
result << arg # I'm closure, I can access result
end
Thread.new do
work.call "from new thread"
end
work.call "from main thread"
# And guess what's inside result now? 🫠
Can you feel how thread-safety problems are coming?
E.g. concurrent-ruby Promise
uses thread pools to execute blocks.
Thread pools doesn't guarantee which thread will execute which block.
work1 = proc do
Thread.current[:state] ||= 'work1'
raise "Unexpected!" if Thread.current[:state] != 'work1'
"result"
end
work2 = proc do
Thread.current[:state] ||= 'work2'
raise "Unexpected!" if Thread.current[:state] != 'work2'
"result"
end
promises = 100.times.flat_map do
Concurrent::Promise.execute(&work1)
Concurrent::Promise.execute(&work2)
end
Concurrent::Promise.zip(*promises).value!
#=> Unexpected! (RuntimeError) 💣💥
NATS is a modern, simple, secure and performant message communications system for microservice world.
nats = NATS.connect("demo.nats.io")
nats.subscribe("service") do |msg|
Thread.current[:counter] ||= 0
Thread.current[:counter] += 1
msg.respond(Thread.current[:counter])
end
Prior version 2.3.0 every subscription was executed in its own thread.
Code above works as expected.
Performance is got much better, but there is a side effect…
nats = NATS.connect("demo.nats.io")
nats.subscribe("service") do |msg|
Thread.current[:counter] ||= 0
Thread.current[:counter] += 1
msg.respond(Thread.current[:counter])
end
Q: So, can I?
A: Not in 2.3.0+! 🤯
Hint: better not to anyway!
- Puma
- Sidekiq
- ActiveRecord
load_async
- NATS client (new!)
- …and many more
Good thing is that you don't have to care about them most of the time.
Pro Tip: In Rails use ActiveSupport::CurrentAttributes
instead of Thread.current
as every request is going to be executed in different thread!
- Blocks are primarily used as callbacks
- Blocks can be called from other threads
- And this thread can be different each time!
- Think twice before using
Thread.current
- Think twice before using
And you have to remember that! {class="text-3xl"}
Our awesome blog: evilmartians.com/chronicles!
See these slides at envek.github.io/osakarubykaigi-threads-callbacks
Footnotes
-
Just let us know in advance in e𝕏-Twitter @evilmartians_jp ↩