Upgrade to Pro — share decks privately, control downloads, hide ads and more …

WebSockets and Ruby : avoiding the pitfalls of multithreading

WebSockets and Ruby : avoiding the pitfalls of multithreading

As web application programers we normally have only to deal with synchronous functions and single threads, because HTTP is a stateless protocol. But now, we have WebSockets, a technology to enable server push easily. This technology can be used to improve user experiences in some cases. For example, when searching is slow, we can show real and detailed progress of the searching to users with WebSockets. But wait! WebSocket brings us multithreaded programing and asynchronous functions. Why is this? And what are the pitfalls when testing Ruby functions that have threads. How can we avoid those pitfalls?


shigeru. nakajima

September 05, 2018

More Decks by shigeru. nakajima

Other Decks in Programming


  1. WebSockets and Ruby : avoiding the pitfalls of multithreading Shigeru

    Nakajima a.k.a. @ledsun Luxiar co., ltd Tokyo Rubyist Meetup 2018/09/05
  2. 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.
  3. 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
  4. 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.
  5. 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
  6. 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.
  7. LODQA I have been implementing LODQA for years. http://lodqa.org/ 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.
  8. 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
  9. 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?’.
  10. 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.
  11. 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?
  12. 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.
  13. 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.
  14. 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.
  15. 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.
  16. 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.
  17. 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
  18. Use with faye-websocket-ruby def call # Create a WebSocket connection.

    ws = Faye::WebSocket.new(env) ws.on :open do async_func do |result| # Send results on the WebSocket connection. ws.send result end end ws.rack_response end
  19. 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.
  20. 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.
  21. 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?
  22. 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.
  23. 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.
  24. 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
  25. 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
  26. Collect assertion with Queue it 'result is details' do queue

    = Queue.new 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!
  27. 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.
  28. 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| curtain.call val end end expect(result).to eq('expected answer') end https://rubygems.org/gems/async_play