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?

https://trbmeetup.doorkeeper.jp/events/78444

shigeru. nakajima

September 05, 2018
Tweet

More Decks by shigeru. nakajima

Other Decks in Programming

Transcript

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

    View full-size slide

  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.

    View full-size slide

  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

    View full-size slide

  4. 1. WebSocket
    improves user
    experiences

    View full-size slide

  5. 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.

    View full-size slide

  6. 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

    View full-size slide

  7. 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.

    View full-size slide

  8. 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.

    View full-size slide

  9. 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

    View full-size slide

  10. 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?’.

    View full-size slide

  11. Summary of 1st part
    WebSocket improves user experiences.

    View full-size slide

  12. 2. WebSocket also
    requires
    multithreaded
    programing

    View full-size slide

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

    View full-size slide

  14. 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.

    View full-size slide

  15. 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?

    View full-size slide

  16. 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.

    View full-size slide

  17. 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.

    View full-size slide

  18. 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.

    View full-size slide

  19. 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.

    View full-size slide

  20. 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.

    View full-size slide

  21. 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

    View full-size slide

  22. 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

    View full-size slide

  23. 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.

    View full-size slide

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

    View full-size slide

  25. 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.

    View full-size slide

  26. 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?

    View full-size slide

  27. 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.

    View full-size slide

  28. 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.

    View full-size slide

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

    View full-size slide

  30. 4. How can we avoid
    this pitfall?

    View full-size slide

  31. 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

    View full-size slide

  32. 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

    View full-size slide

  33. 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!

    View full-size slide

  34. 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.

    View full-size slide

  35. Let’s Play Async!

    View full-size slide

  36. 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

    View full-size slide