Slide 1

Slide 1 text

Threads, callbacks, and execution context in Ruby Andrey Novikov, Evil Martians Osaka Ruby Kaigi #03 09 September 2023

Slide 2

Slide 2 text

About me Hi, I’m Andrey (アンドレイ ) 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

Slide 3

Slide 3 text

Martians are closer than you think Our base is just 30 minutes walk away from here! Please come visit us! 1. Just let us know in advance in e 𝕏 - Twitter @evilmartians_jp ↩︎ [1]

Slide 4

Slide 4 text

Let’s talk about blocks as callbacks It feels like code between do and end is in the same flow with surrounding code, right? WRONG! 3.times do |i| puts "Hello, Osaka RubyKaigi #0#{i}!" end ` ` ` `

Slide 5

Slide 5 text

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)

Slide 6

Slide 6 text

Blocks are called by methods Illustration from the Ruby under a microscope book (日本語版 : Rubyのしくみ ) Ruby under a microscope

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Blocks in time and space When does the block get executed? And where? How to understand? 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?

Slide 9

Slide 9 text

How to understand? No way to know! 😭 Well, except reading method documentation and source code. And memorize, memorize, and memorize. When and where will the block be called?

Slide 10

Slide 10 text

Blocks right here, right now All Enumerable methods are sync, and will call provided block during their execution. E.g. times will yield to the block on every iteration. ` ` 3.times do |i| puts "Hello, Osaka RubyKaigi #0#{i}!" end ` ` ` `

Slide 11

Slide 11 text

Blocks can be called later Much later. ActiveRecord callbacks and also after_commit from after_commit_everywhere gem will store callback proc in ActiveRecord internals for later. after_commit do puts "Hello, Osaka RubyKaigi #03!" end ` ` after_commit_everywhere

Slide 12

Slide 12 text

Blocks can be called from other threads Can you feel how thread-safety problems are coming? 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? 🫠

Slide 13

Slide 13 text

Different threads E.g. concurrent-ruby Promise uses thread pools to execute blocks. Thread pools doesn’t guarantee which thread will execute which block. ` ` raise "Unexpected!" if Thread.current[:state] != 'work1' raise "Unexpected!" if Thread.current[:state] != 'work2' Concurrent::Promise.zip(*promises).value! #=> Unexpected! (RuntimeError) 💣💥 work1 = proc do Thread.current[:state] ||= 'work1' "result" end work2 = proc do Thread.current[:state] ||= 'work2' "result" end promises = 100.times.flat_map do Concurrent::Promise.execute(&work1) Concurrent::Promise.execute(&work2) end

Slide 14

Slide 14 text

Example: NATS client NATS is a modern, simple, secure and performant message communications system for microservice world. Prior version 2.3.0 every subscription was executed in its own thread. Code above works as expected. 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 nats-pure gem

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

Can I use Thread.current in NATS callbacks? Performance is got much better, but there is a side effect… Q: So, can I? A: Not in 2.3.0+! 🤯 Hint: better not to anyway! ` ` 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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Let’s talk more about blocks and callbacks! Attend my next talk about Rails Executor at Tokyo, 27–28 October 2023 Rails Executor talk announce

Slide 20

Slide 20 text

Thank you! @Envek @Envek @Envek @Envek github.com/Envek @evilmartians @evilmartians_jp @evil-martians @evil.martians evilmartians.jp Our awesome blog: evilmartians.com/chronicles! See these slides at envek.github.io/osakarubykaigi-threads-callbacks