Jump to content

Really confused and stuck on web application design of a random chat app (Help appreciated!)


mark1234

Recommended Posts

Hi Forum,

I'm building a random chat application. So far, it works great. Here's the problem though. It only works for 2 simultaneously connected users.

As soon as I have 3 users trying to connect to the "swarm" to get a random chat assignment, well, all calamity ensures. Essentially, the random room assignments then malfunction, creating a terrible user experience.

Essentially, I'm struggling to manage state for multiple connected users in a LAMP environment. I'm using Ably websockets and the PHP SDK for messages, and a few other APIs for video/chat. In addition, I'm using AJAX callbacks in jQuery.

I'm also (as an experiment) using PHP Memcached. I thought it would be simple enough to use it to manage the state of all users across the application using Memcached, but it's harder than I thought. I'm also using the standard php_sessions to manage whether the user is logged in (and authorized to chat) or not.

I realize PHP is perhaps not the ideal language/stack for something like this, perhaps something asynchronous would be better. But it's what I got and am trying to make it work, I don't have time to refactor everything and learn something new.

So my question is how the heck do I do this? Here's the crux of the issue. Suppose:

  • There's 3 users trying to connect simultaneously for a random chat.
  • User 1 immediately gets paired with user 2, and user 3 should simply wait until patiently until there's another user that joins the swarm.
  • The issue is user 1, 2, and 3 are currently all being assigned to a room, that can only hold 2 users at maximum.

I know it sounds awfully simple to manage a swarm of users like this concurrently, but it's not in my experience in part because:

  • When users connect, they need to connect quickly in say 1-2 or 3 seconds maximum, otherwise people may leave the application or no longer be interested or no longer have internet connectivity. This means a way to check and re-check the user's internet connectivity. Currently I'm using an AJAX callback to the server to do this, which then responds with a RoomID to the users.
  • The issue is the server can't tell whether the user doing the callback is user 1 or user 2, or even user 3. It all responds similarly. I've tried to solve this with Memcached by storing an array of all user's requests in the past 5 seconds, and referencing it with the current user. This might be a viable approach, but I'm seeking some guidance from the community before I continue down that rabbit hole. 

I'm probably thinking about this wrongly in some way. But in your opinion, how would you most simply, and most robustly manage state for a swarm of connected users, using AJAX callbacks of a swarm of users? Websockets are a possibility as well, but only publicly broadcasting to all connected users via a REST API. I don't have access to private channels or connected user lists currently. 

Thanks for any help/suggestions!

 

 

Edited by mark1234
Link to comment
Share on other sites

Sounds more like a concurrency problem: you've got two things reading state at the same time, both finding out that the room is available, and both trying to use that room.
Operations need to be atomic, meaning the act of checking for an available room and joining the room need to work as one cohesive unit, with state information that can't be unknowingly modified by something else.

How are you finding a room to assign to a user? Is it something straightforward like "SELECT available room" and then "UPDATE room SET is not available anymore"?

If not that then I don't follow what this "can't tell whether the user is 1 or 2". AJAX polling will start a request, and that request should most definitely be able to identify which user is making it...

Link to comment
Share on other sites

14 hours ago, requinix said:

Sounds more like a concurrency problem: you've got two things reading state at the same time, both finding out that the room is available, and both trying to use that room.
Operations need to be atomic, meaning the act of checking for an available room and joining the room need to work as one cohesive unit, with state information that can't be unknowingly modified by something else.

How are you finding a room to assign to a user? Is it something straightforward like "SELECT available room" and then "UPDATE room SET is not available anymore"?

If not that then I don't follow what this "can't tell whether the user is 1 or 2". AJAX polling will start a request, and that request should most definitely be able to identify which user is making it...

THANKS for the reply! I am using memcache to store state for 5 seconds. And I'm using a SQL update statement when a room becomes "occupied" with 2 users, as not to direct a 3rd user into an occupied room & have an error.

I kind of have it working now, but it's buggy so question both my application architecture & whether I actually have this business logic right. 

The way this works, is both users are making a request to AJAX request to $_POST['user'] and $_POST['number'] to speak to each other, within 5 seconds. (Hence the memcache variable lifetime.)

So if I'm user 3, and you're user 4. You request 3 & 4 to speak with me. Just as I request 3 and 4 to speak with you.

This code sometimes works, but again it's buggy so I'm not satisfied with it. I may need to refactor quite a bit. See anything obvious by chance that pops out? 

 

$user = $_POST['user'];
$number = $_POST['number'];

$memcached = new Memcached();
// Add a server (you may need to adjust this depending on your Memcached setup)
$memcached->addServer('localhost', 11211);

$expiration = 5; // Time in seconds
$memcached->set($user, $number, $expiration);

function check_if_users_joined_room_three($mysqli, $low_user, $high_user){
    $one = 1;
    $query = "SELECT `joined_log` FROM `rooms`
              WHERE `low_user` = ? AND `high_user` = ? AND `joined_log` = ?";
    $stmt = $mysqli->prepare($query);
    $stmt->bind_param("sss", $low_user, $high_user, $one);
    $stmt->execute();
    $stmt->store_result();
    if ($stmt->num_rows > 0) {
       // $stmt->bind_result($joined_log);
       // $stmt->fetch();
        return true;
    } else {
        return false; // Return null if no rows are found
    }
}

function room_joined_log($low_user, $high_user, $mysqli){
    $joined = 1;
    $stmt = $mysqli->prepare("UPDATE `rooms` SET `joined_log` =? WHERE 
    `low_user` = ? AND `high_user` = ?");
    $stmt->bind_param("iss", $joined, $low_user, $high_user);
    $stmt->execute();
    $stmt->store_result();
    if (!$stmt) {
        echo "error:".mysqli_error($mysqli);
    }    
}

function deleteIfExists($mysqli, $users) {
    $query = "DELETE FROM `rooms` WHERE `users` LIKE ?";
    $stmt = $mysqli->prepare($query);
    if (!$stmt) {
        echo 'Error: ', $mysqli->error;
        return false;
    }
    $stmt->bind_param("s", $users);
    if (!$stmt->execute()) {
        echo 'Error: ', $stmt->error;
        return false;
    } else {
        // Check if any rows were affected (deleted)
        $rowsAffected = $stmt->affected_rows;
        // Close the statement
        $stmt->close();
        return $rowsAffected; // Return the number of rows deleted
    }
}

function see_if_exists($mysqli, $users){
    $query = "SELECT `room_id` FROM `rooms` WHERE `users` LIKE ?";
    $stmt = $mysqli->prepare($query);
    if (!$stmt) {
        echo 'Error: ', $mysqli->error;
        return false;
    }
    $stmt->bind_param("s", $users);
    if (!$stmt->execute()) {
        echo 'Error: ', $stmt->error;
        return false;
    }
    $stmt->store_result();
    if ($stmt->num_rows > 0) {
        $stmt->bind_result($room_id);
        $stmt->fetch();
        return $room_id;
    } else {
        return null; // Return null if no rows are found
    }
}
function insert_stuff($users, $room_id, $low_user, $high_user, $mysqli){
    $time = date("Y-m-d H:i:s", time());
    $stmt = $mysqli->prepare("INSERT INTO rooms (users, room_id, inserttime, low_user, high_user, unixtimestamp) 
    VALUES (?,?,?,?,?,?)");
    $stmt->bind_param("ssssss", $users, $room_id, $time, $low_user, $high_user, time());
    $stmt->execute();
    if (!$stmt) {
        echo "error:".mysqli_error($mysqli);
    }
}

    function insert_log($low_user, $high_user, $mysqli){
        $stmt = $mysqli->prepare("INSERT INTO push_notifications (low_user, high_user) 
        VALUES (?,?)");
        $stmt->bind_param("ss", $low_user, $high_user);
        $stmt->execute();
        if (!$stmt) {
            echo "error:".mysqli_error($mysqli);
        }
}

function logmyfolly($low_user,$high_user, $room_id, $mysqli){
    $stmt = $mysqli->prepare("INSERT INTO log_table (low_user, high_user, room_id) 
    VALUES (?,?,?)");
    $stmt->bind_param("sss", $low_user, $high_user, $room_id);
    $stmt->execute();
    if (!$stmt) {
        echo "error:".mysqli_error($mysqli);
    }
}
$randomNumber = rand(0, 99999999);
$roomIDinsert = (string)$randomNumber;

$idvariables = array($user, $number);

sort($idvariables);

$formatted_users = trim($idvariables[0]).",". trim($idvariables[1]);    
$cachedData = $memcached->get($idvariables[0].$idvariables[1]); 

// your friend has already requested to talk to you
if ($cachedData) {
    //echo "cached data found";
   //echo "USER 2";
    $room = see_if_exists($mysqli, $formatted_users); 
   // logmyfolly($idvariables[0],$idvariables[1], $room, $mysqli);
    //ensure we don't direct user into an occupied room
    if(check_if_users_joined_room_three($mysqli, $idvariables[0], $idvariables[1]) != true){
     //   echo "occupied room";
    $arr = array('status' => 1, 'roomID' => $room);
    echo json_encode($arr);
    // update room being occupied
    room_joined_log($idvariables[0], $idvariables[1], $mysqli);
    }
} 
if (!$cachedData) {
    deleteIfExists($mysqli, $formatted_users);
    insert_stuff($formatted_users, $roomIDinsert, $idvariables[0], $idvariables[1], $mysqli);
    $arr = array('status' => 1, 'roomID' => $roomIDinsert);
    echo json_encode($arr);
    $memcached->set($idvariables[0].$idvariables[1], $idvariables[0].$idvariables[1], $expiration); // key, user
} 

 

Link to comment
Share on other sites

6 hours ago, requinix said:

Before I dig much further, does the state problem happen only if both the users (you and me, in your example) request users 3/4 at the same time, or does it also happen if I request first and then you request within ~5 seconds?

EDIT:

Yes. When there's >2 users requesting within 5 seconds, this is where the script seems to be having trouble with misconnections. If it's just 2 users requesting in 5s, it's fine. If the other users are connected in rooms, it's fine. But handling >2 requests in a short period (5 seconds) seems to be the challenge here.

Although I'd add, sometimes just 2 users requesting in <5 results in a room misconnection. 

MYSQL and MEMCACHE are both ATOMIC,  but perhaps mixing the 2 to manage state - isn't going to be ATOMIC anymore. But it seems that that's not the primary issue, since I'm only QA testing this with 2 or 3 users max. It's not like it's getting hundreds of hits per minute causing strange issues.

I have a logging table the looks something like this

281 - NOT CACHED DATA3 - 40981446

282 - NOT CACHED DATA3 - 64097845

283 - NOT CACHED DATA3 - 95710375

284 - NOT CACHED DATA3 - 599601

285 - CACHED DATA3 - 599601

The # is the row, the CACHED/NOT CACHED DATA is depending on which script block it's triggering, and the last # is the generated room number. Whether it's trigging this script block:

if ($cachedData) {
    //echo "cached data found";
   //echo "USER 2";
   // $room = see_if_exists($mysqli, $formatted_users); 
    logmyfolly("CACHED DATA".$idvariables[0],$idvariables[1], $cachedData, $mysqli);
    //ensure we don't direct user into an occupied room
    if(check_if_users_joined_room_three($mysqli, $idvariables[0], $idvariables[1]) != true){
     //   echo "occupied room";
    $arr = array('status' => 1, 'roomID' => $cachedData);
    echo json_encode($arr);
    // update room being occupied
    room_joined_log($idvariables[0], $idvariables[1], $mysqli);
    $memcached->delete($idvariables[0].$idvariables[1]);
    //43291624 - did connect
    //26481673 dind't connect
    //32970404 dind't connect
    //42537685 did connect
    }
} 

or this one:


if (!$cachedData) {

 //   logmyfolly("NOT CACHED DATA".$idvariables[0],$idvariables[1], $roomIDinsert, $mysqli);
//6370375 didnt work , 73342033 didn't work
    logmyfolly("NOT CACHED DATA".$idvariables[0],$idvariables[1], $roomIDinsert, $mysqli);

    deleteIfExists($mysqli, $formatted_users);
    insert_stuff($formatted_users, $roomIDinsert, $idvariables[0], $idvariables[1], $mysqli);
    $arr = array('status' => 1, 'roomID' => $roomIDinsert);
    echo json_encode($arr);
    if (!$memcached->set($idvariables[0].$idvariables[1], $roomIDinsert, $expiration)) {
        echo "Failed to set key '$key' in Memcached.";
    }
    if (!$memcached->set($roomIDinsert, $roomIDinsert, $expiration)) {
        echo "Failed to set key '$key' in Memcached.";
    }

} 

Perhaps there's a memcached library or something else which would be helpful for managing state? 

By the way, this is FIFO. First in first out. There's multiple requests to the script, I want the fastest request gets the room assignment. And the rest to be disappointed. It's because I want the fastest/nearest internet connection to connect with these users.

Anything I much appreciate your eagerness to help! I puzzled on this all day yesterday, it's going to take a minor-miracle to fix it in the time I need it for! But that's my fault lol.

Edited by mark1234
update info about script connection I miscommunicated
Link to comment
Share on other sites

Sorry for the triple post. The program seems to be working about  3/4th's of the time, but 25% of the time the users get sent into the wrong rooms because memcache didn't store the variable apparently? 

I have the logging going with memcache. As you see in the 2 highlighted lines, there's "no cached data" 2 times in a row. This is the malfunction whereby the users are sent into two different rooms, instead of the same. With it's every other one cached/not cached, it works fine.

Any ideas what could be causing this behavior? I just started using memcache for the first time so I'm not super familiar. 

Screenshot 2023-11-02 at 9.48.42 PM.png

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.