How can I manage to place an overflow of participants in optimally created dynamic Rooms?

As a follow up to my other question regarding random pairings, I've hit another interesting engineering problem. At the moment, I have my group room set to the max limit of 50 participants. However, I expect to have 1000+ participants arrive when this campaign launches. It is not important that all 1000 of these participants are placed in the same room but it is important that there is a room available for them. And it would be best, if this room had several other participants in it. So, can you recommend a way of placing users into rooms at scale?

In the past, I've handled this in a rather hacky way based on thrown join errors. If the user was not allowed in Room 1, I'd try to place them in Room 2. And so on, or err.. as many errors as it took. Using this sad logic, if the first 10 rooms were full, the user would have to wait through 10 errors in order to get placed accordingly.

I thought about using the REST API to check how many participants were in each room in order to recommend an available room but I suspect this will still cause issues if multiple users were all recommended the same room at once.

Another idea is to estimate the amount of participants we might receive, create X number of rooms, and place those visitors into a random room rather than placing them in a linear fashion. A more distributed approach but would need to finesse room quantity manually.

I realize this is less of a Twilio question and more of an engineering problem but I was wondering how you would approach it. Any help is much appreciated!

Answers

  • shelbyz
    shelbyz ✭✭✭

    You may still be able to use the room join error logic but with a twist. Have an endpoint that attempts a conditional update (using revision property with ifMatch) on the sync doc with the room name. If it succeeds create a new room using that name, if it fails read the sync doc for the latest room name. In either case return that latest room for the client to join.

    When a client logs in, read the sync doc for the latest room and attempt joining. If failure, call that endpoint and have it return the room to join.

    I could not find a good example of using revision property but it would be fairly similar to this (just add revision values on the sync doc to ifMatch property to the update request - https://www.twilio.com/docs/sync/api/document-resource?code-sample=code-update-a-document-using-the-rest-api&code-language=Node.js&code-sdk-version=3.x

  • leemartin
    leemartin ✭✭✭
    edited July 2021

    Hey @shelbyz, is this suggestion assuming I'm using Twilio Sync (I'm not) or is sync docs part of Twilio Video somehow? Regardless, this solution certainly flew over my head. 😅

    For additional context, my followers and I are discussing general system design solutions here:

  • leemartin
    leemartin ✭✭✭

    Here's a little function which gets all rooms and then gets all participants in those room. From here, I can figure out which rooms still have available spots and return those room names as an array to the client before connect() (The client chooses at random) If there isn't an available room, I simply create a brand new one with a random id.

    const client = require('twilio')(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)
    const { v4: uuidv4 } = require('uuid')
    
    exports.handler = async (event, context) => {
      try {
        // get all rooms
        let rooms = await client.video.rooms.list()
    
        // get all participants from all rooms
        await Promise.all(rooms.map(async (room) => {
          let participants = await client.video.rooms(room.sid).participants.list()
    
          room.participantCount = participants.length
        }))
    
        // get available rooms
        let availableRooms = rooms.filter(room => {
          return room.maxParticipants != room.participantCount
        })
    
        let names
    
        if (availableRooms.length) {
          // available room names
          names = availableRooms.map(room => {
            return room.uniqueName
          })
    
        } else {
          // new room name
          names = [`leemartin-${uuidv4()}`]
    
        }
    
        return {
          statusCode: 200,
          body: JSON.stringify(names)
        }
    
      } catch (error) {
        return {
          statusCode: 500,
          body: error.toString()
        }
      }
    }
    
    

    I'm not a huge fan of this solution but I'm trying to be proactive on this client project. Questions

    • Does client.video.rooms.list() return ALL rooms?
    • Can I get the participant count when calling rooms.list() vs calling participants.list()?
  • shelbyz
    shelbyz ✭✭✭

    Yeah I was suggesting making use of Twilio Sync (Docs specifically), as you may run into unexpected problems having each users make a query to list all available rooms and find ones with capacity. It would work for small numbers but as the system scales returning a large list of rooms and then iterating over every room will take an increasing large amount of time.

  • leemartin
    leemartin ✭✭✭

    Cheers @shelbyz. I'm not very familiar with Twilio Sync. Any chance you could provide a simple explanation of the service and how your solution solves the problem?

  • @leemartin A little birdie told me you had a call with the support team to get this sorted.😃🐦

    Do you mind sharing your findings here to help out future community members? I would greatly appreciate it!

  • leemartin
    leemartin ✭✭✭

    Yes! I had a great call with Brian about potentially using Sync to manage a sort of realtime inventory. Don't worry, I will share my solution once I cobble it together. Both here and on the blog: https://leemartin.dev So inspired by this platform!

  • Amazing, happy to hear it! Thank you, @leemartin. ✨

  • shelbyz
    shelbyz ✭✭✭

    @leemartin - sorry the notifications never reached an inbox, it sounds like you got sorted on Sync. Let me know where it all lands.

  • Thanks again for all the help here. The experience is now live: https://www.voyeurist.io

    So, in the end, I did not use Sync. Instead, I used the Twilio Video webhook to manage a simple representation of Rooms in a DynamoDb. Each record contains:

    • roomSid
    • currentParticipants count
    • maxParticipants count

    On room-created, a new Room record is created with default properties.

    On room-ended, the Room record is deleted.

    On participant-connected, I increment the currentParticipants attribute by 1.

    On participant-disconnected, I decrement the currentParticipants attribute by 1.

    Note: a room may be destroyed before you receive the last participant-disconnected notification, so plan accordingly.

    This allows me to then scan for an available room, which are defined as rooms which are not full, sorted by the smallest amount of participants. If I can't find a room, I simply return a randomly named new one. (This works because of client side room creation.)

    Here's what that available rooms function looks like.

    module.exports.available = async event => {
      const params = {
        TableName: `${process.env.stage}-underoath-rooms`
      }
    
      try {
        const result = await dynamoDb.scan(params).promise()
    
        // All rooms
        let rooms = result.Items
    
        // Rooms which are not full
        let availableRooms = rooms.filter(room => {
          return room.maxParticipants != room.currentParticipants
        })
    
        // Rooms sorted by smallest amount of participants
        availableRooms.sort((a, b) => {
          return a.currentParticipants - b.currentParticipants
        })
    
        // Make a map of room names
        let roomNames = availableRooms.map(room => {
          return room.roomName
        })
    
        if (availableRooms.length) {
          // Available rooms exist
          return success(roomNames)
        } else {
          // Create a new room
          return success([`underoath-${uuidv4()}`])
        }
    
      } catch (error) {
        console.error(error)
    
        return failure({
          status: false
        })
    
      }
    }
    

    This solution works great when dealing with a huge burst of traffic but it doesn't scale back well as things start to calm. In hindsight, I probably wouldn't find rooms with the smallest amount of participants and simply keep trying to fill rooms from the top. That way the solution wouldn't attempt to distribute users over many many low occupied rooms.

    Anyway, hopefully this is helpful for others facing this issue!

If this is an emergency, please contact Twilio Support. This is not an official Support channel. https://support.twilio.com/
Have an urgent question?
Please contact Twilio Support. This is not an official Support channel.
Contact Support