Skip to content

Commit

Permalink
Do not use URLs that were in previously used and saved in DB
Browse files Browse the repository at this point in the history
  • Loading branch information
recap committed May 28, 2024
1 parent e8376c8 commit 9ba2818
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 57 deletions.
18 changes: 17 additions & 1 deletion UserDb.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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) => {
Expand All @@ -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.")
})
}
Expand Down
2 changes: 2 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"experiments": [
{
"name": "guess_two_thirds",
"enabled": false,
"scheduler": {
"type": "GatScheduler",
"params": {
Expand All @@ -11,6 +12,7 @@
},
{
"name": "public_goods_game",
"enabled": true,
"scheduler": {
"type": "GatScheduler",
"params": {
Expand Down
85 changes: 40 additions & 45 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -51,6 +50,15 @@ process.on("SIGINT", function () {
* @type {Set<string>}
*/
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)) {
Expand Down Expand Up @@ -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)) {
Expand All @@ -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)
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
})

Expand All @@ -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]
Expand All @@ -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}`)
Expand Down
50 changes: 39 additions & 11 deletions test/test_virtual_user.js
Original file line number Diff line number Diff line change
@@ -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)

3 changes: 3 additions & 0 deletions test/virtual-user-ws.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down

0 comments on commit 9ba2818

Please sign in to comment.