Slide 1

Slide 1 text

WebSockets and Ruby : avoiding the pitfalls of multithreading Shigeru Nakajima a.k.a. @ledsun Luxiar co., ltd Tokyo Rubyist Meetup 2018/09/05

Slide 2

Slide 2 text

My Experience I have implemented asynchronous functions in Ruby on Rails applications to support WebSockets. I was confused about how to test functions that have threads with RSpec.

Slide 3

Slide 3 text

Summary I will talk about... 1. Using WebSockets to improve user experience 2. WebSockets' requirement of multithreaded programing and asynchronous functions 3. RSpec's pitfall when testing Ruby functions that have threads 4. How we can avoid those pitfalls

Slide 4

Slide 4 text

1. WebSocket improves user experiences

Slide 5

Slide 5 text

Background For long time, Web application provide static HTML pages. As web application programers, we have to deal only with synchronous functions and single threads, because HTTP is a stateless protocol. But now, we have WebSockets. WebSockets enable us to perform a server push easily. Server push technologies can improve user experiences under some circumstances.

Slide 6

Slide 6 text

WebSocket fit to Ruby on Rails » WebSocket is easier than other push message technology, e.g. e-mail, sms, push notifications, as only needs browser » WebSocket uses less memories than other push technology, e.g. long polling, Sever-sent events

Slide 7

Slide 7 text

Example use case One example is dynamic searching. A web server provides search results according to a query input by a user. Searching time varies considerably depending on the input query, as does the number of results. We can use WebSocket for this situation. We provide parts of the answers for queries as soon any results are found.

Slide 8

Slide 8 text

LODQA I have been implementing LODQA for years. LODQA is a search engine for linked data. Many linked data servers provide SPARQL API. SPARQL is a SQL like language for computer. SPARQL is too difficult for almost human. Many people want natural language interface. LODQA provide a natural language interface.

Slide 9

Slide 9 text

Sequence After WebSocket is established: 1. Server receives a query in natural language 2. Server generates SPARQLs from the query 3. Server invoke SPARQL queries on backend linked data servers 4. Server send back answers as soon as any are found

Slide 10

Slide 10 text

Seeing is believing Without server push technologies, we were able to only provide all the answers at once or no answers. Now, we can provide answers as fast as we retrieve them. User can quickly see the initial answers and can evaluate ‘Does my query match my question or not?’.

Slide 11

Slide 11 text

Summary of 1st part WebSocket improves user experiences.

Slide 12

Slide 12 text

2. WebSocket also requires multithreaded programing

Slide 13

Slide 13 text

Once upon a time We traditionally wrote synchronous functions for web applications.

Slide 14

Slide 14 text

Why we could write synchronous functions? Web servers create a new thread per received HTTP request. The server keeps receiving the next request in the existing thread. We don't have to care about subsequent requests. We can use a thread to respond to received request, and can treat it synchronously.

Slide 15

Slide 15 text

WebSocket requires async functions To support WebSocket, a web application needs asynchronous functions. Why? Because server can’t create threads automatically for WebSocket messages. Why?

Slide 16

Slide 16 text

Messages may be very very many We send and receive messages on WebSocket connections. Type of messages and amount of messages depend on the application. If we want to send many small messages frequently, the number of messages may be ten thousands. Can we create threads for each messages? They will consume a huge amount of memory. This problem is very like the C10K problem.

Slide 17

Slide 17 text

Event loop model architecture fits to C10K We know "Event loop model architecture" is a nice solution for the C10K problem. It has only one thread and the control flow is switched at waiting IO(Input/Output). The server reads events from IO, and switches control flow to the event handler for the event. After that event handler returns, the server reads the next event from IO.

Slide 18

Slide 18 text

How to implemen Event loop Ruby community has EventMachine gem. The EventMachine gem provides event loop model architecture. This gem is nice and popular. Actually, faye-websocket-ruby and other gems for WebSocket use it. We can support WebSocket by using EventMachine.

Slide 19

Slide 19 text

Event loop model brings a new strict Event loop model has only one thread. If you spent long time in one event handler, all subsequent events will wait until finishing it. We MUST NOT run slow functions in any event handlers.

Slide 20

Slide 20 text

To run slow functions, create a new thread If we need to run slow functions, we create a thread by ourselves and run the function in it. We can return event handler immediately. When the slow function finishes, we send messages on the WebSocket connection. We only need the reference of the WebSocket connection.

Slide 21

Slide 21 text

Sample implementation def async_func Thread.start do yield slow_function end end » Create a new thread » Run slow function » Return result by calling block

Slide 22

Slide 22 text

Use with faye-websocket-ruby def call # Create a WebSocket connection. ws = ws.on :open do async_func do |result| # Send results on the WebSocket connection. ws.send result end end ws.rack_response end

Slide 23

Slide 23 text

We can support WebSocket We can write asynchronous functions with a thread. And We can support WebSocket. Note WebSocket might also requires multithreaded programing. Next, multithreaded programing brings us a new next problem about testing.

Slide 24

Slide 24 text

3. RSpec has a pitfalls when testing Ruby functions that have threads

Slide 25

Slide 25 text

Test async functions with RSpec RSpec is a major testing framework. I want to use RSpec to test asynchronous functions. But, it is more difficult than imaginary.

Slide 26

Slide 26 text

RSpec code for async_func it 'is expected answer' do async_func do |result| expect(result).to eq('expected answer') end end Does this spec success?

Slide 27

Slide 27 text


Slide 28

Slide 28 text

Will this spec fail? it 'is expected answer' do async_func do |result| expect(result).not_to eq('expected answer') end end to was changed to not_to We expect that this assertion will fail.

Slide 29

Slide 29 text

No! Why?

Slide 30

Slide 30 text

RSpec captures failed by exception Look Spec code carefully. it 'is expected answer' do async_func do |result| # --- in sub thread --- expect(result).not_to eq('expected answer') # --- in sub thread --- end end When assertions are fail, RSpec will raise exception. But, we cannot capture exceptions that occurred in other threads.

Slide 31

Slide 31 text

OMG! Tests that never fail are not helpful. So how can we avoid this pitfall?

Slide 32

Slide 32 text

4. How can we avoid this pitfall?

Slide 33

Slide 33 text

How can we make this spec failed? Important things are » Assertions should run in the main thread » Ruby has the Queue class to wait other threads

Slide 34

Slide 34 text

How to use the Queue 1. One thread pop queue to wait any data from other threads 2. The waiting thread restarts and get data when another thread push any object to the queue

Slide 35

Slide 35 text

Collect assertion with Queue it 'result is details' do queue = async_func do |r| # --- in sub thread --- queue.push r # --- in sub thread --- end # Wait to `queue.push` from other threads. result = queue.pop # Resume next code when `queue.push` is called. expect(result).not_to eq('expected answer') end Spec fails as expected!

Slide 36

Slide 36 text

Conclusion 1. We can write test codes for asynchronous functions. 2. We can write asynchronous functions for WebSocket with test and confidence. 3. We can provide better user experience in Ruby on Rails applications with WebSocket.

Slide 37

Slide 37 text

Let’s Play Async!

Slide 38

Slide 38 text

AsyncPlay gem To avoid boilerplate code, I created 'async_play' gem. it 'result is details' do result = AsyncPlay.opening do |curtain| async_func do |val| val end end expect(result).to eq('expected answer') end

Slide 39

Slide 39 text

Thank you!