Skip to content

Commit

Permalink
improve concurrent join/joinOrCreate. bump 0.11.10
Browse files Browse the repository at this point in the history
  • Loading branch information
endel committed Sep 7, 2019
1 parent aed25b8 commit 813cf98
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 51 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "colyseus",
"version": "0.11.9",
"version": "0.11.10",
"description": "Multiplayer Game Server for Node.js.",
"main": "./lib/index.js",
"typings": "./lib/index.d.ts",
Expand Down
98 changes: 57 additions & 41 deletions src/MatchMaker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { merge } from './Utils';
import { merge, retry } from './Utils';

import { Client, generateId, isValidId } from './index';
import { IpcProtocol, Protocol } from './Protocol';
Expand All @@ -10,7 +10,8 @@ import { LocalPresence } from './presence/LocalPresence';
import { Presence } from './presence/Presence';

import { debugAndPrintError, debugMatchMaking } from './Debug';
import { MatchMakeError } from './Errors';
import { MatchMakeError } from './errors/MatchMakeError';
import { SeatReservationError } from './errors/SeatReservationError';
import { MatchMakerDriver, RoomListingData } from './matchmaker/drivers/Driver';
import { LocalDriver } from './matchmaker/drivers/LocalDriver';

Expand Down Expand Up @@ -46,13 +47,15 @@ export class MatchMaker {
}

public async joinOrCreate(roomName: string, options: ClientOptions) {
let room = await this.queryRoom(roomName, options);
return await retry(async () => {
let room = await this.queryRoom(roomName, options);

if (!room) {
room = await this.createRoom(roomName, options);
}
if (!room) {
room = await this.createRoom(roomName, options);
}

return this.reserveSeatFor(room, options);
return this.reserveSeatFor(room, options);
}, 5, [SeatReservationError]);
}

public async create(roomName: string, options: ClientOptions) {
Expand All @@ -68,13 +71,15 @@ export class MatchMaker {
}

public async join(roomName: string, options: ClientOptions) {
const room = await this.queryRoom(roomName, options);
return await retry(async () => {
const room = await this.queryRoom(roomName, options);

if (!room) {
throw new MatchMakeError(`no rooms found with provided criteria`, Protocol.ERR_MATCHMAKE_INVALID_CRITERIA);
}
if (!room) {
throw new MatchMakeError(`no rooms found with provided criteria`, Protocol.ERR_MATCHMAKE_INVALID_CRITERIA);
}

return this.reserveSeatFor(room, options);
return this.reserveSeatFor(room, options);
});
}

public async joinById(roomId: string, options: ClientOptions) {
Expand Down Expand Up @@ -119,25 +124,25 @@ export class MatchMaker {
return await this.driver.find(conditions);
}

public async queryRoom(roomName: string, options: ClientOptions) {
await this.awaitRoomAvailable(roomName);

const handler = this.handlers[roomName];
if (!handler) {
throw new MatchMakeError(`no available handler for "${roomName}"`, Protocol.ERR_MATCHMAKE_NO_HANDLER);
}
public async queryRoom(roomName: string, options: ClientOptions): Promise<RoomListingData> {
return await this.awaitRoomAvailable(roomName, async () => {
const handler = this.handlers[roomName];
if (!handler) {
throw new MatchMakeError(`no available handler for "${roomName}"`, Protocol.ERR_MATCHMAKE_NO_HANDLER);
}

const query = this.driver.findOne({
locked: false,
name: roomName,
...handler.getFilterOptions(options),
});
const query = this.driver.findOne({
locked: false,
name: roomName,
...handler.getFilterOptions(options),
});

if (handler.sortOptions) {
query.sort(handler.sortOptions);
}
if (handler.sortOptions) {
query.sort(handler.sortOptions);
}

return await query;
return await query;
});
}

public async remoteRoomCall<R= any>(
Expand Down Expand Up @@ -255,7 +260,6 @@ export class MatchMaker {
return room.listing;
}

// used only for testing purposes
public getRoomById(roomId: string) {
return this.localRooms[roomId];
}
Expand Down Expand Up @@ -289,7 +293,10 @@ export class MatchMaker {
sessionId, room.roomId, this.processId,
);

await this.remoteRoomCall(room.roomId, '_reserveSeat', [sessionId, options]);
const [_, reserveSeatSuccessful] = await this.remoteRoomCall(room.roomId, '_reserveSeat', [sessionId, options]);
if (!reserveSeatSuccessful) {
throw new SeatReservationError(`${room.roomId} is already full.`);
}

return { room, sessionId };
}
Expand Down Expand Up @@ -371,25 +378,34 @@ export class MatchMaker {
this.presence.del(room.roomId);
}

protected async awaitRoomAvailable(roomToJoin: string) {
const key = this.getHandlerConcurrencyKey(roomToJoin);
const concurrency = await this.presence.incr(key) - 1;
protected async awaitRoomAvailable(roomToJoin: string, callback: Function): Promise<RoomListingData> {
return new Promise(async (resolve, reject) => {
const concurrencyKey = this.getHandlerConcurrencyKey(roomToJoin);
const concurrency = await this.presence.incr(concurrencyKey) - 1;

this.presence.decr(key);
// avoid having too long timeout if 10+ clients ask to join at the same time
const concurrencyTimeout = Math.min(concurrency * 100, REMOTE_ROOM_SHORT_TIMEOUT);

if (concurrency > 0) {
// avoid having too long timeout if 10+ clients ask to join at the same time
const concurrencyTimeout = Math.min(concurrency * 100, REMOTE_ROOM_SHORT_TIMEOUT);

debugMatchMaking(
'receiving %d concurrent requests for joining \'%s\' (waiting %d ms)',
concurrency, roomToJoin, concurrencyTimeout,
);

return await new Promise((resolve, reject) => setTimeout(resolve, concurrencyTimeout));
} else {
return true;
}

setTimeout(async () => {
try {
const result = await callback();
resolve(result);

} catch (e) {
reject(e);

} finally {
await this.presence.decr(concurrencyKey);
}
}, concurrencyTimeout);
});
}

protected getRoomChannel(roomId: string) {
Expand Down
2 changes: 1 addition & 1 deletion src/Room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ export abstract class Room<T= any> extends EventEmitter {
// skip next checks if client has reconnected successfully (through `allowReconnection()`)
if (client.state === ClientState.RECONNECTED) { return; }

// dispose immediatelly if client reconnection isn't set up.
// try to dispose immediatelly if client reconnection isn't set up.
await this._decrementClientCount();
}

Expand Down
2 changes: 1 addition & 1 deletion src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { generateId } from '.';
import { registerNode, unregisterNode } from './discovery';
import { LocalPresence } from './presence/LocalPresence';

import { MatchMakeError } from './Errors';
import { MatchMakeError } from './errors/MatchMakeError';
import { Protocol } from './Protocol';

export type ServerOptions = IServerOptions & {
Expand Down
14 changes: 8 additions & 6 deletions src/Utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import querystring from 'querystring';

import { debugAndPrintError } from './Debug';
import { debugAndPrintError, debugMatchMaking } from './Debug';

//
// nodemon sends SIGUSR2 before reloading
Expand All @@ -16,20 +16,22 @@ export function registerGracefulShutdown(callback) {
export function retry(
cb: Function,
maxRetries: number = 3,
retries: number = 0,
errorWhiteList: any[] = [],
retries: number = 0,
) {
return new Promise((resolve, reject) => {
cb()
.then(resolve)
.catch((e) => {
if (
errorWhiteList.indexOf(e.constructor) === -1 &&
errorWhiteList.indexOf(e.constructor) !== -1 &&
retries++ < maxRetries
) {
retry(cb, maxRetries, retries, errorWhiteList).
then(resolve).
catch((e2) => reject(e2));
setTimeout(() => {
retry(cb, maxRetries, errorWhiteList, retries).
then(resolve).
catch((e2) => reject(e2));
}, Math.floor(Math.random() * Math.pow(2, retries) * 400));

} else {
reject(e);
Expand Down
1 change: 0 additions & 1 deletion src/Errors.ts → src/errors/MatchMakeError.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export class MatchMakeError extends Error {
public code: number;

constructor(message: string, code: number) {
super(message);
this.code = code;
Expand Down
5 changes: 5 additions & 0 deletions src/errors/SeatReservationError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class SeatReservationError extends Error {
constructor(message: string) {
super(message);
}
}

0 comments on commit 813cf98

Please sign in to comment.