Simple publish-subscribe 

messaging system 


example based on  kanbanery.com



Artur Trzop @ lunarlogic.io
2014-01-14

What is Faye?

  • pub/sub messaging system

  • based on the Bayeux protocol

  • provides message servers for Node.js and Ruby

  • client lib on server and browser side


Faye is great for...


Chat
Activity Streams
Notifications
Collaboration
Multiplayer games
Realtime data
Dashboards
2nd Screen experiences

More about Faye


Faye is just like any other Rack app



Storage layer: 

  • memory  
  • redis (let you scale Faye service)

Faye architecture


  • based on the  Bayeux protocol  

    operations:
    handshake
    connect
    disconnect
    subscribe
    unsubscribe
    publish

  • Server-side/Client-side extensions
  • Bayeux protocol



    • protocol for transporting asynchronous messages

    • primarily over HTTP

    • low latency between a server and web clients

     

    Faye Server



    • object-based implementation of Bayeux

    • delegates execution of Bayeux operations to the Engine


    • easy to swap out the backend implementation

    • does not know anything about HTTP or other network transports

     


    Clustering

    Redis

    Faye




    Faye Adapters




  • Persistent connections using  WebSocket 


  • Long-polling via HTTP POST 

  • Faye adapters


  • Cross Origin Resource Sharing 

    XHR to another domain
    modern browsers
    supports many types of HTTP requests



  • Callback-polling via  JSON-P 

    supports old browsers
    only GET requests
  • Ruby Sever (Faye)

    faye.ru - rackup config file
    require File.expand_path('../config/environment',  __FILE__)
    
    bayeux = Faye::RackAdapter.new(
        :mount => '/faye',
        :timeout => 25
    )
    
    # some code
    
    run bayeux
    

    Ruby Sever (Faye)

    faye.ru - rackup config file
    # ...
    
    bayeux.add_extension(PushOnlyServer.new)
    bayeux.add_extension(ServerAuth.new)
    
    bayeux.bind(:subscribe) do |client_id, channel|
      Rails.logger.info "#{client_id} subscribed #{channel}"
    end
    
    # ...
    

    Run Ruby Server (Faye)



    $ thin --rackup faye.ru -e development -p 8090 start

    http://127.0.0.1:8090/faye

    lib for browser clients
    http://127.0.0.1:8090/faye/client.js

    Run Ruby Server (Faye)
    on CI



    tip for circleci.yml

    nohup bash -c "bundle exec thin --rackup faye.ru -e test 
    -p 8090 start &"

    Faye Extensions

    # ...
    class FayeExtension
      def incoming(message, callback)
        # ...
        callback.call(message)
      end
    
      def outgoing(message, callback)
        # ...
        callback.call(message)
      end
    end
    # ...
    

    Push Only Server Extension

    class PushOnlyServer
      def incoming(msg, callback)
        unless msg['channel'] =~ /^\/meta\//
          password = msg['ext'] &&
                     msg['ext']['password']
    
          if password != AppConfig.faye.push_secret
            msg['error'] = '403::Password required'
          end
        end
    
        callback.call(msg)
      end
      # ...
    end
    

    Push Only Server Extension

    class PushOnlyServer
      # ...
      
      def outgoing(message, callback)
        unless message['ext'].nil?
          message['ext'].delete('password')
        end
    
        callback.call(message)
      end
    end
    

    Publish message on Rails server side

    class LiveUpdate
      def self.send_to_faye(...)
        # ...
          message = {
            :channel => 'channel-name',
            :data => {
              :event => 'event-name',
              :faye_socket_id => 'id'
            },
            :ext => {
              :password => 'secret'
            }
          }
          uri = URI.parse(internal_url)
          Net::HTTP.post_form(uri,
                   :message => message.to_json)
       # ...
      end
    end
    

    Server Auth Extension

    class ServerAuth
      def incoming(msg, callback)
        # ...
        # some code on the next slide
        # ...
    
        # Call the server back now we're done
        callback.call(msg)
      end
    end
    

    Server Auth Extension

    # ...
    
        # Let non-subscribe messages through
        
        unless msg['channel'] == '/meta/subscribe'
          return callback.call(msg)
        end
        
    # ...
    

    Server Auth Extension

    # ...
    
        if msg['ext'].nil?
          msg['error'] = 'Missing ext key'
        else
          user_id = msg['ext']['user_id']
          
          # check if user can subscribe the channel
          # ...
          # more code on the next slide
        end
    
    # ...
    

    Server Auth Extension

    # ...
    
    user_id = message['ext']['user_id']
    faye_token = message['ext']['faye_token']
    project_id = message['ext']['project_id']
    
    project = Project.get(project_id)
          
    if project.present?
      # ... next slide
    else
      message['error'] = "Project doesn't exist"
    end
    
    # ...
    

    Server Auth Extension

    
    # check if user has access to project etc
    user = User.get(user_id)
    
    if user && user.valid_faye_token?(faye_token)
      client_id = message['fayeClientId']
      # ...
    
      $semaphore.synchronize do
        $user_activities[client_id] = some_id
      end
    elsif user
      message['error'] = "Invalid faye token"
    else
      message['error'] = "User doesn't exist"
    end
    
    # ...
    

    faye.ru - Ruby Server with Mutex

    
    # key: faye client id
    # value: list of project memberships ids
    $user_activities = {}
    
    $semaphore = Mutex.new
    $update_thread = Thread.new do
      loop do
        sleep 60
        $semaphore.synchronize do
          ids = $user_activities.values.uniq
          # ...
          Project.update_activities(ids)
        end
      end
    end
    

    Browser client

    Subscribing to channels



    The * wildcard matches any channel segment. 

    So /foo/* matches /foo/bar and /foo/thing 
    but not /foo/bar/thing.

    Browser client

    Subscribing to channels



    The ** wildcard matches any channel name recursively.

     So /foo/** matches 
    /foo/bar
    /foo/thing 
    and /foo/bar/thing.

    Browser client

    # CoffeeScript
    
    class K2.FayeConnection
      
      connectAndBindEvents: ->
        # ...
        url = 'http://localhost:8090/faye'
        @faye_client = new Faye.Client url,
          timeout: timeout
    
        # client-side extension (next slide)
        
      # ...
    

    Browser client
    Client-side extension

    # ...
    
    @faye_client.addExtension outgoing: (message, callback) ->
      
      message.ext = message.ext or {}
      
      message.ext.user_id = K2.faye.currentUserId
      message.ext.faye_token = K2.faye.fayeToken
      message.ext.project_id = K2.faye.projectId
      
      callback(message)
      
    # ...
    

    Browser client
    Bind events

    class K2.FayeConnection
      # ...  
        @faye_client.bind 'transport:up', ->
          # the client is online
          
          console.log "New socket ID: " + faye_connection.faye_client._0
          
          # stop interval refreshing kanban board
          faye_connection.polling.stop()
          faye_connection.polling.fullRefresh()
    

    Browser client
    Bind events

    class K2.FayeConnection
      # ...  
        @faye_client.bind 'transport:down', ->
          # the client is offline
          
          # update data with interval
          # kanbanery stuff - not related to faye
          faye_connection.polling.start() 
          faye_connected = false
    

    Browser client
    Subscribing to channel

    class K2.FayeConnection
      subscribeToChannelAndBindEvents: ->
        
        subscription = @faye_client.subscribe @fayeChannelName(), (msg) ->
          
          console.log "GOT MESSAGE", msg
          
          # current faye socket id
          id = faye_connection.faye_client._0
          
          # ignore message sent by client
          if msg.faye_socket_id != id
            faye_connection.store.refresh msg.event
    

    Browser client
    Subscription callbacks

    
    # subscription has been set up
    # and acknowledged by the server
    
    subscription.callback ->
      console.log "SUBSCRIBED"
      faye_connection.polling.stop()
    
    # error while creating subscription
    subscription.errback (error) ->
      console.log(error.message)
      faye_connection.polling.start()
    

    Check 2nd screen experiences

    on


    Thanks

    Links

    Alternatives  for Faye
    https://github.com/maccman/juggernaut (deprecated)
    http://socket.io
    Alternatives hosted API 

    http://pusher.com
    http://www.pubnub.com
    Other:
    http://railscasts.com/episodes/260-messaging-with-faye
    http://net.tutsplus.com/tutorials/ruby/how-to-use-faye-as-a-real-time-push-server-in-rails/
    https://blog.jcoglan.com/2012/06/09/why-you-should-never-use-hash-functions-for-message-authentication/

    Made with Slides.com