Slide 1

Slide 1 text

Vladimir Dementyev Evil Martians Untangling Cables & Demystifying Twisted Transistors 2023 1. It's Rails 2. Freak on a Cable 3. Y'all Want a Stream 4. Make Me ping 5. Wire Tongue 6. Threaded Transistor 7. Throw me & Forget 8. Turbo Streams Everywhere 9. (Not) Alone I Reconnect Bonus: See You on the Other Cable Lyrics by Vladimir "Palkan" Dementyev Produced by Evil Martians, inc.

Slide 2

Slide 2 text

1. It's Rails

Slide 3

Slide 3 text

Rails is a software development philosophy 3 palkan_tula palkan

Slide 4

Slide 4 text

Conceptual Compression 4 palkan_tula palkan

Slide 5

Slide 5 text

Unboxing time! 5 palkan_tula palkan

Slide 6

Slide 6 text

Unboxing time! 5 palkan_tula palkan

Slide 7

Slide 7 text

2. Freak on a Cable

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

github.com/palkan

Slide 10

Slide 10 text

github.com/palkan evilmartians.com

Slide 11

Slide 11 text

github.com/palkan evilmartians.com anycable.io

Slide 12

Slide 12 text

The Book on abstraction layers from and for Rails 8 palkan_tula palkan

Slide 13

Slide 13 text

3. Y'all Want a Stream

Slide 14

Slide 14 text

palkan_tula palkan Channels Layer 10 class ChatChannel < ApplicationCable::Channel def subscribed @room = Room.find_by(id: params[:id]) return reject unless @room stream_for @room end def speak(data) broadcast_to(@room, event: "newMessage", user_id: user.id, body: data.fetch("body") ) end end

Slide 15

Slide 15 text

11 palkan_tula palkan class ChatChannel < ApplicationCable::Channel def subscribed @room = Room.find_by(id: params[:id]) return reject unless @room stream_for @room end def speak(data) broadcast_to(@room, event: "newMessage", user_id: user.id, body: data.fetch("body") ) end end import { createConsumer } from "@rails/actioncable" let cable = createConsumer() let sub = cable.subscriptions.create( {channel: "ChatChannel", id: 2023}, { received({user_id, body}) { console.log(`Message from ${user_id}:${body}`); } } ) sub.perform("speak", {body: "Hoi!"})

Slide 16

Slide 16 text

12

Slide 17

Slide 17 text

palkan_tula palkan 13 Protocol Web Server Action Cable Server I/O loop Rails Executor Connection Channel Pub/Sub Subscriber Map Client Socket Consumer Web Socket Sub Pubsub Adapter Monitor Channel Channel Worker Pool Internal Pool Sub Sub Map of Action Cable HTTP rack.hijack #connect #subscribed { command: "subscribe" } OPEN { command: "message" } #receive CLOSE #disconnect #unsubscribed #stream_from #subscribe #unsubscribe broadcast #invoke_callback #send

Slide 18

Slide 18 text

4. Make Me Ping

Slide 19

Slide 19 text

palkan_tula palkan @rails/actioncable 15 import { createConsumer } from "@rails/actioncable" let cable = createConsumer() cable.subscriptions.create( {channel: "ChatChannel", id: 2023}, { received({user_id, body}) { console.log(`Message from ${user_id}:${body}`); } } )

Slide 20

Slide 20 text

palkan_tula palkan @rails/actioncable 16 import { createConsumer } from "@rails/actioncable" let cable = createConsumer() cable.subscriptions.create( {channel: "ChatChannel", id: 2023}, { received({user_id, body}) { console.log(`Message from ${user_id}:${body}`); } } ) ~ WebSocket connection Abstraction to subscribe for and receive updates

Slide 21

Slide 21 text

palkan_tula palkan One consumer per session (e.g., a browser tab) 17 @rails/actioncable

Slide 22

Slide 22 text

palkan_tula palkan github.com/le0pard/cable-shared-worker 18

Slide 23

Slide 23 text

palkan_tula palkan One consumer per session (e.g., a browser tab) Many subscriptions per consumer (multiplexing) 19 @rails/actioncable

Slide 24

Slide 24 text

palkan_tula palkan One consumer per session (e.g., a browser tab) Many subscriptions per consumer (multiplexing) — how many? 20 @rails/actioncable

Slide 25

Slide 25 text

We may* have as many subscriptions as canals in Amsterdam 21 palkan_tula palkan * Doesn't mean we should though Consumer Monitor Sub Sub Sub Web Socket Sub Sub

Slide 26

Slide 26 text

palkan_tula palkan @rails/actioncable 22 import { createConsumer } from "@rails/actioncable" let cable = createConsumer() let a = cable.subscriptions.create("UserChannel") // SUBSCRIBE let b = cable.subscriptions.create("UserChannel") // a.unsubscribe() // b.unsubscribe() // UNSUBSCRIBE

Slide 27

Slide 27 text

palkan_tula palkan One consumer per session (e.g., a browser tab) Many subscriptions per consumer Monitor to keep consumer connected 23 @rails/actioncable

Slide 28

Slide 28 text

palkan_tula palkan Keep alive 24 Consumer Monitor Web Socket stale timer

Slide 29

Slide 29 text

palkan_tula palkan Keep alive 24 Consumer Monitor Web Socket ping stale timer

Slide 30

Slide 30 text

palkan_tula palkan Keep alive 24 Consumer Monitor Web Socket stale timer reconnect

Slide 31

Slide 31 text

palkan_tula palkan Action Cable Pings Help to identify broken TCP connections (from both sides, but not always) 25

Slide 32

Slide 32 text

palkan_tula palkan Action Cable Pings Help to identify broken TCP connections (from both sides, but not always) Hardly configurable (and may be too frequent to cause battery drain for mobile devices) 25

Slide 33

Slide 33 text

26 palkan_tula palkan # config/application.rb # Re-define internal constant module ActionCable::Server::Connections BEAT_INTERVAL = 10 end import { ConnetionMonitor } from "@rails/actioncable" // Adjust stale timeout (2xBEAT_INTERVAL) ConnectionMonitor.staleThreshold = 20

Slide 34

Slide 34 text

palkan_tula palkan More clients github.com/anycable/anycable-client 27

Slide 35

Slide 35 text

palkan_tula palkan More clients github.com/anycable/anycable-client 27

Slide 36

Slide 36 text

5. Wire Tongue

Slide 37

Slide 37 text

Protocol is a language in which the server and client speak to each other 29 palkan_tula palkan Protocol Web Server Consumer Web Socket HTTP

Slide 38

Slide 38 text

palkan_tula palkan docs.anycable.io 30

Slide 39

Slide 39 text

31 palkan_tula palkan { "command":"subscribe", "identifier":"{\"channel\" \"UserChannel\"}" } { "command":"message", "identifier":"{\"channel\":\"UserChannel\"}", "data":"{\"action\":\"speak\",\"text\":\"hoi\"}" } { "command":"unsubscribe", "identifier":"{\"channel\":\"UserChannel\"}" } {"type":"welcome"} { "type":"confirm_subscription", "identifier":"{\"channel\":\"UserChannel\"}" } { "type":"reject_subscription", "identifier":"{\"channel\":\"UserChannel\"}" } { "identifier":"{\"channel\":\"UserChannel\"}", "message":"{\"text\":\"hoi\"}" } {"type":"ping"} { "type":"disconnect", "reason":"unauthorized", "reconnect":false } No type!

Slide 40

Slide 40 text

palkan_tula palkan Protocol is incomplete No IDs (session, message) No perform ACKs No error handling 32

Slide 41

Slide 41 text

palkan_tula palkan 33 let cable = createConsumer() for(let i=0; i<10; i++) { let sub = cable.subscriptions.create("UserChannel") sub.unsubscribe() } // Will I be subscribed or not? cable.subscriptions.create("UserChannel")

Slide 42

Slide 42 text

palkan_tula palkan github.com/palkan/wsdirector - loop: multiplier: 10 actions: - send: data: &sub command: "subscribe" identifier: "{\"channel\":\"BenchmarkChannel\"}" - send: data: command: "unsubscribe" identifier: "{\"channel\":\"BenchmarkChannel\"}" - send: <<: *sub - send: data: command: "message" identifier: "{\"channel\":\"BenchmarkChannel\"}" data: "{\"action\":\"echo\",\"test\":42}" - receive: data: identifier: "{\"channel\":\"BenchmarkChannel\"}" message: {action: "echo", test: 42} 34

Slide 43

Slide 43 text

35

Slide 44

Slide 44 text

36

Slide 45

Slide 45 text

36 RuntimeError - Unable to find subscription with identifier: {"channel":"BenchmarkChannel"} RuntimeError - Already subscribed to {"channel":"BenchmarkChannel"}

Slide 46

Slide 46 text

palkan_tula palkan Solution? 37

Slide 47

Slide 47 text

38 (First rows only note) Some random Rails application

Slide 48

Slide 48 text

palkan_tula palkan 39 let cable = createConsumer() for(let i=0; i<10; i++) { let sub = cable.subscriptions.create("UserChannel") sub.unsubscribe() } // Will I be subscribed or not? It depends. Why so? cable.subscriptions.create("UserChannel")

Slide 49

Slide 49 text

6. Threaded Transistor

Slide 50

Slide 50 text

palkan_tula palkan 41 Web Server Web Socket HTTP Action Cable Executor

Slide 51

Slide 51 text

palkan_tula palkan 41 Web Server Action Cable Server Web Socket HTTP rack.hijack Action Cable Executor

Slide 52

Slide 52 text

palkan_tula palkan 41 Web Server Action Cable Server I/O loop Client Socket Web Socket HTTP rack.hijack Action Cable Executor

Slide 53

Slide 53 text

palkan_tula palkan 41 Web Server Action Cable Server I/O loop Client Socket Web Socket Worker Pool HTTP rack.hijack OPEN Action Cable Executor

Slide 54

Slide 54 text

palkan_tula palkan 41 Web Server Action Cable Server I/O loop Rails Executor Client Socket Web Socket Worker Pool HTTP rack.hijack OPEN Action Cable Executor

Slide 55

Slide 55 text

palkan_tula palkan 41 Web Server Action Cable Server I/O loop Rails Executor Connection Client Socket Web Socket Worker Pool HTTP rack.hijack #connect OPEN Action Cable Executor

Slide 56

Slide 56 text

palkan_tula palkan 41 Web Server Action Cable Server I/O loop Rails Executor Connection Channel Client Socket Web Socket Worker Pool HTTP rack.hijack #connect {command: "subscribe"} OPEN #subscribed Action Cable Executor

Slide 57

Slide 57 text

palkan_tula palkan 41 Web Server Action Cable Server I/O loop Rails Executor Connection Channel Client Socket Web Socket Worker Pool HTTP rack.hijack #connect {command: "subscribe"} OPEN {command: "message"} #receive #subscribed Action Cable Executor

Slide 58

Slide 58 text

palkan_tula palkan 41 Web Server Action Cable Server I/O loop Rails Executor Connection Channel Client Socket Web Socket Worker Pool HTTP rack.hijack #connect #unsubscribed {command: "subscribe"} OPEN {command: "message"} #receive CLOSE #disconnect #subscribed Action Cable Executor

Slide 59

Slide 59 text

palkan_tula palkan Action Cable Server Thread pool executor (4 threads by default) Must be taken into account for shared resources (e.g., database pool) Uses Rails executor to cleanup after work 42

Slide 60

Slide 60 text

43 palkan_tula palkan class Connection identified_by :user def connect self.user = find_verified_user Current.account = user.account end end

Slide 61

Slide 61 text

44 palkan_tula palkan class Connection identified_by :user def connect self.user = find_verified_user # BAD — will be reset at the end # of the #handle_open call Current.account = user.account end end

Slide 62

Slide 62 text

class Connection identified_by :user before_command do # GOOD — set for every command Current.account = user.account end def connect self.user = find_verified_user end end 44 palkan_tula palkan class Connection identified_by :user def connect self.user = find_verified_user # BAD — will be reset at the end # of the #handle_open call Current.account = user.account end end

Slide 63

Slide 63 text

palkan_tula palkan Units of Work Lifecycle callbacks (connect, disconnect) 45

Slide 64

Slide 64 text

palkan_tula palkan Units of Work Lifecycle callbacks (connect, disconnect) Incoming commands 45

Slide 65

Slide 65 text

palkan_tula palkan Units of Work Lifecycle callbacks (connect, disconnect) Incoming commands Outgoing messages 45

Slide 66

Slide 66 text

7. Throw me & Forget

Slide 67

Slide 67 text

palkan_tula palkan 47 Rails Executor Connection Pub/Sub Subscriber Map Client Socket Pubsub Adapter Channel Worker Pool Internal Pool Pub/Sub

Slide 68

Slide 68 text

palkan_tula palkan 47 Rails Executor Connection Pub/Sub Subscriber Map Client Socket Pubsub Adapter Channel Worker Pool Internal Pool broadcast message Pub/Sub

Slide 69

Slide 69 text

palkan_tula palkan 47 Rails Executor Connection Pub/Sub Subscriber Map Client Socket Pubsub Adapter Channel Worker Pool Internal Pool broadcast message #broadcast Pub/Sub

Slide 70

Slide 70 text

palkan_tula palkan 47 Rails Executor Connection Pub/Sub Subscriber Map Client Socket Pubsub Adapter Channel Worker Pool Internal Pool broadcast message #invoke_callback #broadcast Pub/Sub

Slide 71

Slide 71 text

palkan_tula palkan 47 Rails Executor Connection Pub/Sub Subscriber Map Client Socket Pubsub Adapter Channel Worker Pool Internal Pool broadcast message #invoke_callback #broadcast #stream_from callback is executed Pub/Sub

Slide 72

Slide 72 text

palkan_tula palkan 47 Rails Executor Connection Pub/Sub Subscriber Map Client Socket Pubsub Adapter Channel Worker Pool Internal Pool broadcast message #invoke_callback #send #broadcast #transmit #stream_from callback is executed Pub/Sub

Slide 73

Slide 73 text

palkan_tula palkan 47 Rails Executor Connection Pub/Sub Subscriber Map Client Socket Pubsub Adapter Channel Worker Pool Internal Pool Pub/Sub at-most once

Slide 74

Slide 74 text

48 The pitfalls of realtime-ification, RailsConf, 2022

Slide 75

Slide 75 text

palkan_tula palkan Yet Another Pitfall 49 100.times { ActionCable.server.broadcast "test", {text: "Count: #{_1}"} } # Will a client receive 1,2,3,4,...,99 in order?

Slide 76

Slide 76 text

50

Slide 77

Slide 77 text

51

Slide 78

Slide 78 text

palkan_tula palkan And One More 52 def subscribed stream_from "test" # Q: Will this client receive the message? ActionCable.server.broadcast "test", {text: "Welkom!"} end

Slide 79

Slide 79 text

Are you tired of threads? 53 palkan_tula palkan I am

Slide 80

Slide 80 text

8. Turbo Streams Everywhere

Slide 81

Slide 81 text

palkan_tula palkan Turbo Streams z Automatic subscriptions via HTML () Built-in Turbo::StreamsChannel Zero application code to connect cables 55

Slide 82

Slide 82 text

palkan_tula palkan Turbo Streams 56 www.my-hot.app

Slide 83

Slide 83 text

palkan_tula palkan Turbo Streams 54 www.my-hot.app SUBSCRIBE Connection Channel

Slide 84

Slide 84 text

palkan_tula palkan DOM update Turbo Streams 54 www.my-hot.app HTML Action Cable broadcast from anywhere Connection Channel

Slide 85

Slide 85 text

palkan_tula palkan UNSUBSCRIBE Turbo Streams 54 www.my-hot.app Connection

Slide 86

Slide 86 text

palkan_tula palkan Turbo Streams Prone to subscribe/unsubscribe race conditions Managed in a decentralized way (via HTML partials) 57

Slide 87

Slide 87 text

58

Slide 88

Slide 88 text

59 anycable.substack.com

Slide 89

Slide 89 text

9. (Not) Alone I ReConnect

Slide 90

Slide 90 text

palkan_tula palkan Server Restart N clients re-connect N clients re-subscribe each to M channels each Threaded executor's work queue is: (N+1)*M 61

Slide 91

Slide 91 text

palkan_tula palkan Server Restart Threaded executor's work queue is: (N+1)*M Server can not keep up with confirmations, clients re-issue subscribe commands—! 62

Slide 92

Slide 92 text

palkan_tula palkan Server Restart Threaded executor's work queue is: (N+1)*M Server can not keep up with confirmations, clients re-issue subscribe commands—! 62 Thundering Herd

Slide 93

Slide 93 text

palkan_tula palkan Server Restart Threaded executor's work queue is: (N+1)*M Server can not keep up with confirmations, clients re-issue subscribe commands—! 62 Connection Avalanche

Slide 94

Slide 94 text

Connection avalanches lead to significant load spikes 63 palkan_tula palkan

Slide 95

Slide 95 text

How to protect Rails from connection avalanches? 64 palkan_tula palkan

Slide 96

Slide 96 text

palkan_tula palkan Avalanche Protection Reconnect with backoff & jitter 65

Slide 97

Slide 97 text

66

Slide 98

Slide 98 text

palkan_tula palkan Avalanche Protection Reconnect with backoff & jitter Merge subscriptions 67

Slide 99

Slide 99 text

palkan_tula palkan Avalanche Protection Reconnect with backoff & jitter Merge subscriptions Avoid slow calls in #subscribed 67

Slide 100

Slide 100 text

palkan_tula palkan Avalanche Protection Reconnect with backoff & jitter Merge subscriptions Avoid slow calls in #subscribed Serialize subscribe commands 67

Slide 101

Slide 101 text

palkan_tula palkan 68 import { createCable } from "@anycable/web" let cable = createCable({ concurrentSubscribes: false })

Slide 102

Slide 102 text

palkan_tula palkan Isolate WebSocket and regular HTTP clusters 69 Avalanche Protection

Slide 103

Slide 103 text

palkan_tula palkan Isolate WebSocket and regular HTTP clusters AnyCable: disconnect-less deploys 69 Avalanche Protection

Slide 104

Slide 104 text

palkan_tula palkan Isolate WebSocket and regular HTTP clusters AnyCable: disconnect-less deploys AnyCable: resumable sessions 69 Avalanche Protection

Slide 105

Slide 105 text

See you on the other Cable

Slide 106

Slide 106 text

palkan_tula palkan 71 Map of AnyCable Protocol+ AnyCable-Go I/O poll Rails Executor Connection Channel Cache Session Cable Web Socket Channel Monitor Goroutine Pools Sub HTTP #connect #subscribed #disconnect #unsubscribed broadcast Encoder Transport Hub Sub Channel Channel Logger gRPC Pool RPC Controller Read Chan Write Chan Broadcaster Broker Hub Pub/Sub Shard Shard subscribe OPEN DATA CLOSE

Slide 107

Slide 107 text

Thank You Slides: evilmartians.com/events Twitter: @palkan_tula, @evilmartians, @any_cable