diff --git a/UserDb.js b/UserDb.js index b3e92fb..fad6558 100644 --- a/UserDb.js +++ b/UserDb.js @@ -3,6 +3,7 @@ const murmurhash = require("murmurhash") const fs = require("fs").promises const User = require("./user.js") +let writeFlag = false class UserDb extends Map { constructor(file) { @@ -20,6 +21,8 @@ class UserDb extends Map { data.forEach((u) => { const user = new User(u.userId, u.experimentId) user.redirectedUrl = u.redirectedUrl + user.experimentUrl = u.experimentUrl + user.groupId = u.groupId user.oTreeId = u.oTreeId user.tokenParams = u.tokenParams user.state = u.redirectedUrl ? u.state : user.state @@ -38,6 +41,13 @@ class UserDb extends Map { return u.userId == userId }) } + getUsedUrls(){ + return Array.from(this.values()).filter((u) => { + return (u.experimentUrl) + }).map((u) => { + return u.experimentUrl + }) + } dump() { const data = [] this.forEach((v, k) => { @@ -52,15 +62,21 @@ class UserDb extends Map { data.push(v.serialize()) }) - const dump = JSON.stringify(data) + const dump = JSON.stringify(data, null, 2) const h = murmurhash(dump, this.seed) console.log(`${h}:${this.lastHash}:${dump}`) if (this.lastHash !== h) { + if (writeFlag) { + console.log('WARN FILe STILL WRITING') + } + writeFlag = true fs.writeFile(this.file, dump) .then(() => { + writeFlag = false this.lastHash = h }) .catch((err) => { + writeFlag = false console.error("[ERROR] Writing UserDb to file.") }) } diff --git a/config.json b/config.json index 4f14d31..3786164 100644 --- a/config.json +++ b/config.json @@ -2,6 +2,7 @@ "experiments": [ { "name": "guess_two_thirds", + "enabled": false, "scheduler": { "type": "GatScheduler", "params": { @@ -11,6 +12,7 @@ }, { "name": "public_goods_game", + "enabled": true, "scheduler": { "type": "GatScheduler", "params": { diff --git a/server.js b/server.js index 715664e..7a56049 100644 --- a/server.js +++ b/server.js @@ -7,7 +7,6 @@ const socketIO = require("socket.io") const app = express() const server = http.createServer(app) const io = socketIO(server) -console.log(io) const fs = require("fs") const jwt = require("jsonwebtoken") const CryptoJS = require("crypto-js") @@ -51,6 +50,15 @@ process.on("SIGINT", function () { * @type {Set} */ const usedUrls = new Set() +if (options.resetDb && fs.existsSync(userDbFile)) { + console.log(`Deleting ${userDbFile} file!`) + fs.unlinkSync(userDbFile) +} +/** + * + * @type {Map<`${userId}:${experimentId}`, User>} + */ +const usersDb = new UserDb(userDbFile) function getOrSetValue(obj, key, defaultValue) { if (!(key in obj)) { @@ -192,6 +200,7 @@ const validateHmac = (req, res, next) => { async function getExperimentUrls(experiments) { const expToEnable = config.experiments.map((e) => e.name) + const usedUrlsFromDb = usersDb.getUsedUrls() // clear existing URLs first: for (const [_, val] of Object.entries(experiments)) { @@ -213,7 +222,7 @@ async function getExperimentUrls(experiments) { }) const expUrls = getOrSetValue(exp.servers, r.server, []) //console.log(`expUrls ${expUrls} oTreeUrls: ${r.experimentUrl}`) - if (expUrls.includes(r.experimentUrl) || usedUrls.has(r.experimentUrl)) { + if (expUrls.includes(r.experimentUrl) || usedUrls.has(r.experimentUrl) || usedUrlsFromDb.includes(r.experimentUrl)) { return } expUrls.push(r.experimentUrl) @@ -245,9 +254,11 @@ function startReadyGames(experiments, agreementIds, usersDb) { const experiment = experiments[experimentId] const scheduler = experiment.scheduler if (!scheduler) { - console.error( - `[ERROR] No scheduler set for experiment ${experimentId}. Check config.json.`, - ) + if (experiment.enabled) { + console.error( + `[ERROR] No scheduler set for experiment ${experimentId}. Check config.json.`, + ) + } continue } @@ -292,8 +303,8 @@ function startReadyGames(experiments, agreementIds, usersDb) { agreement.server, ) - console.log(agreedUsersIds) - console.log(nonAgreedUsersIds) + //console.log(agreedUsersIds) + //console.log(nonAgreedUsersIds) //agreedUsersIds.forEach((userId) => { agreedUsersIds.forEach((compoundKey) => { @@ -334,46 +345,29 @@ function startReadyGames(experiments, agreementIds, usersDb) { async function main() { const experiments = {} - if (options.resetDb && fs.existsSync(userDbFile)) { - console.log(`Deleting ${userDbFile} file!`) - fs.unlinkSync(userDbFile) - } - /** - * - * @type {Map<`${userId}:${experimentId}`, User>} - */ - const usersDb = new UserDb(userDbFile) + // if (options.resetDb && fs.existsSync(userDbFile)) { + // console.log(`Deleting ${userDbFile} file!`) + // fs.unlinkSync(userDbFile) + // } + // /** + // * + // * @type {Map<`${userId}:${experimentId}`, User>} + // */ + // const usersDb = new UserDb(userDbFile) + // Load database from file await usersDb.load() + // Get used URLs from database const agreementIds = {} + // Load URLs from oTree servers + await getExperimentUrls(experiments) + const expToEnable = config.experiments.map((e) => e.name) // Load schedulers from directory // initialize returns a new ClassLoader const SchedulerPlugins = await ClassLoader.initialize("./schedulers") try { - // Get oTree experiment URLs from servers - otreeData = await getOtreeUrls(otreeIPs, otreeRestKey) - // Build experiments object - otreeData.forEach((r) => { - const exp = getOrSetValue(experiments, r.experimentName, { - name: r.experimentName, - enabled: expToEnable.includes(r.experimentName), - servers: {}, - }) - const expUrls = getOrSetValue(exp.servers, r.server, []) - if (expUrls.includes(r.experimentUrl)) { - return - } - expUrls.push(r.experimentUrl) - }) - } catch (error) { - console.log( - `[ERROR] Failed to retrieve data from oTree server/s reason: ${error.message}`, - ) - process.exit(1) - } - try { // Go through each experiment config and // load the appropriate scheduler class and // attach it to the experiments object @@ -561,7 +555,7 @@ async function main() { }) io.on("connection", (socket) => { - console.log('WS: connection') + //console.log('WS: connection') socket.on("landingPage", async (msg) => { const userId = msg.userId const experimentId = msg.experimentId @@ -572,11 +566,10 @@ async function main() { // queued events. switch (user.state) { case "queued": - console.log(`User ${userId} already in state ${user.state}.`) user.changeState("queued") break case "inoTreePages": - console.log(`User ${userId} already in state ${user.state}.`) + console.log(`RE-REDIRECT ${userId}.`) const expUrl = user.redirectedUrl socket.emit("gameStart", { room: expUrl.toString() }) break @@ -636,7 +629,7 @@ async function main() { // User sends agreement to start game socket.on("userAgreed", (data) => { - console.log("[SOCKET][userAgreed] ", data) + //console.log("[SOCKET][userAgreed] ", data) const userId = data.userId const uuid = data.uuid const experimentId = data.experimentId @@ -651,7 +644,7 @@ async function main() { // If everyone agrees, start game if (agreement.agree(compoundKey)) { console.log("Start Game!") - startGame(agreement.agreedUsers, agreement.urls, agreement.experimentId) + startGame(agreement.agreedUsers, agreement.urls, agreement.experimentId, agreement.agreementId) } }) @@ -667,7 +660,7 @@ async function main() { * @param users {string[]} * @param urls {string[]} */ - function startGame(users, urls, experimentId) { + function startGame(users, urls, experimentId, agreementId) { console.log(`Starting game with users: ${users} and urls ${urls}.`) for (let i = 0; i < users.length; i++) { const userId = users[i] @@ -690,7 +683,9 @@ async function main() { //const participantCode = expUrl.pathname.split("/").pop() const sock = user.webSocket // Emit a custom event with the game room URL - user.redirectedUrl = `${expUrl}?participant_label=${user.userId}` + user.experimentUrl = expUrl + user.groupId = agreementId + user.redirectedUrl = `${expUrl}?participant_label=${user.userId}&group_id=${user.groupId}` sock.emit("gameStart", { room: user.redirectedUrl }) user.changeState("inoTreePages") console.log(`Redirecting user ${user.userId} to ${user.redirectedUrl}`) diff --git a/test/test_virtual_user.js b/test/test_virtual_user.js index 0bd0a42..dd029c7 100644 --- a/test/test_virtual_user.js +++ b/test/test_virtual_user.js @@ -1,15 +1,43 @@ const VirtualUser = require('./virtual-user-ws') +const fs = require('fs') -vu01 = new VirtualUser("01234", "public_goods_game", "http://localhost:8060") -vu02 = new VirtualUser("05678", "public_goods_game", "http://localhost:8060") -vu03 = new VirtualUser("91011", "public_goods_game", "http://localhost:8060") +function randomBetween(min, max) { + return Math.floor( + Math.random() * (max - min) + min + ) +} -vu01.connect().then(() => { - vu01.attemptNormalQueueFlow() -}) -vu02.connect().then(() => { - vu02.attemptNormalQueueFlow() -}) -vu03.connect().then(() => { - vu03.attemptNormalQueueFlow() +const maxUsers = 1000 +const experimentId = "public_goods_game" +const url = "http://localhost:8060" +const virtUsers = {} + +for (let i = 0; i < maxUsers; i++) { + const id = 1000 + i + //const id = randomBetween(1, 9999999) + vu = new VirtualUser(id, experimentId, url) + virtUsers[id] = vu +} + +Object.values(virtUsers).forEach(vu => { + vu.connect().then(() => { + vu.attemptNormalQueueFlow() + }) }) + +setTimeout(() => { + const ids = Object.values(virtUsers).filter(vu => { + if (vu.state == "redirected"){ + return true + } + }).map(vu => { + return vu.userId + }) + const idsString = JSON.stringify(ids, null, 2) + fs.writeFile('redirected_users.json', idsString, (err) => { + if (err) { + console.log('Error ', err) + } + }) +}, 5000) + diff --git a/test/virtual-user-ws.js b/test/virtual-user-ws.js index 24d922d..15fd14c 100644 --- a/test/virtual-user-ws.js +++ b/test/virtual-user-ws.js @@ -62,13 +62,16 @@ class VirtualUser { userId: this.userId, uuid: uuid, }) + this.state = "agreed" }) this.socket.on("gameStart", (data) => { console.log(`[${this.userId}] redirected to ${data.room}.`) + this.state = "redirected" }) this.socket.on("disconnect", () => { console.log(`[${this.userId}] disconnected from ${this.serverUrl}`) this.socket = null + this.state = "disconnected" }) this.socket.emit("landingPage", { experimentId: this.experimentId, diff --git a/user.js b/user.js index 0de9490..37602f1 100644 --- a/user.js +++ b/user.js @@ -5,6 +5,8 @@ class User { this.tokenParams = null this.oTreeId = null this.redirectedUrl = null + this.experimentUrl = null + this.groupId = null this.state = "new" this.listeners = [] this.webSocket = null @@ -16,6 +18,8 @@ class User { userId: this.userId, experimentId: this.experimentId, redirectedUrl: this.redirectedUrl, + groupId: this.groupId, + experimentUrl: this.experimentUrl, state: this.state, oTreeId: this.oTreeId, tokenParams: this.tokenParams,