Skip to content

Latest commit

 

History

History
610 lines (389 loc) · 15.5 KB

File metadata and controls

610 lines (389 loc) · 15.5 KB
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.
persist
provider fallback local sans serif mono
none
false
Martian Grotesk, Martian Mono
Martian Grotesk
Martian Grotesk
Martian Mono
4/3
slide-left
Threads, callbacks, and execution context in Ruby
true

Threads, callbacks, and execution context in Ruby

Andrey Novikov, Evil Martians
Osaka Ruby Kaigi #03
09 September 2023
Evil MartiansEvil Martians
Osaka Ruby Kaigi #03
<style> a { border-bottom: none !important; } </style>

layout: image-right image: ./images/20230305_193526.jpg class: annotated-list

About me

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


layout: image-right image: ./images/osaka-venue-martian-office-map.png

Martians are closer than you think

Our base is just 30 minutes walk away from here!

Please come visit us! 1

Evil Martians logo


Let's talk about blocks as callbacks

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!


Blocks are separate pieces of code

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)

Blocks are called by methods

Illustration from the Ruby under a microscope book (日本語版: Rubyのしくみ)


layout: statement

Blocks ARE callbacks

We often use blocks as callbacks to hook our own behavior for someone's else code.


Blocks in time and space

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?

layout: statement

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.


Blocks right here, right now

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.


Blocks can be called later

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.

after_commit_everywhere


Blocks can be called from other threads

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?


Different threads

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) 💣💥

Example: NATS client

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.

nats-pure.rb


layout: image image: /images/nats-pure-thread-pool-pull-request.png


Can I use Thread.current in NATS callbacks?

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!


Where you can find thread pools?

  • 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!


Recap

  • 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

And you have to remember that! {class="text-3xl"}


Let's talk more about blocks and callbacks!

Attend my next talk about Rails Executor at

Kaigi on Rails 2023

Tokyo, 27–28 October 2023


Thank you!

<style> ul a { border-bottom: none !important; } ul { list-style-type: none !important; } ul li { margin-left: 0; padding-left: 0; } </style>

Footnotes

  1. Just let us know in advance in e𝕏-Twitter @evilmartians_jp