Skip to content

Commit

Permalink
Resolve socket server overload
Browse files Browse the repository at this point in the history
  • Loading branch information
szczz committed Feb 17, 2025
1 parent e62b236 commit 0465138
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 60 deletions.
51 changes: 43 additions & 8 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -580,24 +580,40 @@ function logger(type, message) {
console.log(`${currentDate} [${type}] ${message}`);
}

const clients = new Set();

// Used to broadcast messages to all connected clients
function broadcastToClients(message){
const payload = JSON.stringify(message);
clients.forEach((client) => {
if(client.readyState === WebSocket.OPEN){
logger('INFO', `Payload sent to the client`);
client.send(payload);
}
});
}

wss.on('connection', (ws) => {
logger('INFO', `A client connected to the web socket server.`);
clients.add(ws);

// The following messages can be received in stringified JSON format:
// { uuid: <uuid>, lock: true }
// { uuid: <uuid>, lock: false }
ws.on('message', function (msg) {
const message = JSON.parse(msg);
const uuid = message.uuid;
const {uuid, lock} = message;

if (!uuid) {
ws.send(JSON.stringify({ status: 'fail', message: 'UUID not provided.' }));
}
logger('INFO', `${msg}`);
// User wants to lock storyline since they are about to load/edit it.
if (message.lock) {
// Unlock any storyline that the user was previously locking.
delete lockedUuids[ws.uuid];
if (lock) {
const currentLock = lockedUuids[uuid];

// Someone else is currently accessing this storyline, do not allow the user to lock!
if (!!lockedUuids[uuid] && ws.uuid !== uuid) {
if (currentLock && ws.uuid !== uuid) {
logger('INFO', `A client failed to lock the storyline ${uuid}.`);
ws.send(JSON.stringify({ status: 'fail', message: 'Another user has locked this storyline.' }));
}
Expand All @@ -610,6 +626,11 @@ wss.on('connection', (ws) => {
lockedUuids[uuid] = secret;
ws.uuid = uuid;
ws.send(JSON.stringify({ status: 'success', secret }));

broadcastToClients({
type:'lock',
uuid,
});
}
} else {
// Attempting to unlock a different storyline, other than the one this connection has locked, so do not allow.
Expand All @@ -625,9 +646,14 @@ wss.on('connection', (ws) => {
// Unlock the storyline for any other user/connection to use.
else {
logger('INFO', `A client successfully unlocked the storyline ${uuid}.`);
delete ws.uuid;
delete lockedUuids[uuid];
delete ws.uuid;
ws.send(JSON.stringify({ status: 'success' }));

broadcastToClients({
type:'unlock',
uuid,
});
}
}
});
Expand All @@ -636,9 +662,18 @@ wss.on('connection', (ws) => {
logger('INFO', `Client connection with web socket server has closed.`);
// Connection was closed, unlock this user's locked storyline
if (ws.uuid) {
delete lockedUuids[ws.uuid];
delete ws.uuid;
const currentLock = lockedUuids[ws.uuid];
if (currentLock) {
logger('INFO', `Releasing lock on storyline ${ws.uuid} after connection closed`);
delete lockedUuids[ws.uuid];
broadcastToClients({
type: 'unlock',
uuid: ws.uuid,
});
}
}

clients.delete(ws);
});
});

Expand Down
26 changes: 16 additions & 10 deletions src/components/metadata-editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,7 @@ import {
import { VueSpinnerOval } from 'vue3-spinners';
import { VueFinalModal } from 'vue-final-modal';
import { useUserStore } from '../stores/userStore';
import { computed } from "vue";
import JSZip from 'jszip';
import axios from 'axios';
Expand Down Expand Up @@ -646,7 +647,7 @@ export default class MetadataEditorV extends Vue {
@Prop({ default: true }) editExisting!: boolean; // true if editing existing storylines product, false if new product
currentRoute = window.location.href;
user = computed(() => useUserStore().userProfile.userName || 'Guest');
configs: {
[key: string]: StoryRampConfig | undefined;
} = { en: undefined, fr: undefined };
Expand Down Expand Up @@ -751,7 +752,7 @@ export default class MetadataEditorV extends Vue {
// Initialize Storylines config and the configuration structure.
this.configs = { en: undefined, fr: undefined };
this.configFileStructure = undefined;
// set any metadata default values for creating new product
if (!this.loadExisting) {
// set current date as default
Expand Down Expand Up @@ -849,7 +850,15 @@ export default class MetadataEditorV extends Vue {
// If a product UUID is provided, fetch the contents from the server.
if (this.$route.params.uid) {
this.generateRemoteConfig();
this.generateRemoteConfig().catch(() => {
// Handle any connection/lock errors here
Message.error(this.$t('editor.editMetadata.message.error.unauthorized'));
if (this.$route.name === 'editor') {
setTimeout(() => {
this.$router.push({ name: 'home' });
}, 2000);
}
});
}
}
Expand Down Expand Up @@ -1017,10 +1026,9 @@ export default class MetadataEditorV extends Vue {
this.controller = new AbortController();
this.loadStatus = 'loading';
const user = useUserStore().userProfile.userName || 'Guest';
const secret = this.lockStore.secret;
fetch(this.apiUrl + `/retrieve/${this.uuid}/${version}`, {
headers: { user, secret: secret },
headers: { user: this.user, secret: secret },
signal: this.controller.signal
})
.then((res: Response) => {
Expand Down Expand Up @@ -1143,9 +1151,8 @@ export default class MetadataEditorV extends Vue {
return;
}
this.loadStatus = 'loading';
const user = useUserStore().userProfile.userName || 'Guest';
const secret = this.lockStore.secret;
fetch(this.apiUrl + `/history/${this.uuid}`, { headers: { user, secret } }).then((res: Response) => {
fetch(this.apiUrl + `/history/${this.uuid}`, { headers: { user: this.user, secret } }).then((res: Response) => {
if (res.status === 404) {
// Product not found.
this.loadStatus = 'waiting';
Expand Down Expand Up @@ -1210,7 +1217,7 @@ export default class MetadataEditorV extends Vue {
// First, hit the Express server `rename` endpoint to perform the `rename` syscall on the file system.
await axios
.post(this.apiUrl + `/rename`, {
user: userStore.userProfile.userName || 'Guest',
user: this.user,
previousUuid: prevUuid,
newUuid: this.changeUuid,
configs: { en: convertedEnglish, fr: convertedFrench }
Expand Down Expand Up @@ -1631,10 +1638,9 @@ export default class MetadataEditorV extends Vue {
this.configFileStructure?.zip.generateAsync({ type: 'blob' }).then((content: Blob) => {
const formData = new FormData();
formData.append('data', content, `${this.uuid}.zip`);
const userStore = useUserStore();
const headers = {
'Content-Type': 'multipart/form-data',
user: userStore.userProfile.userName || 'Guest',
user: this.user,
secret: this.lockStore.secret
};
Message.warning(this.$t('editor.editMetadata.message.wait'));
Expand Down
92 changes: 50 additions & 42 deletions src/stores/lockStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,59 +12,67 @@ export const useLockStore = defineStore('lock', {
result: {} as any,
broadcast: undefined as BroadcastChannel | undefined,
confirmationTimeout: undefined as NodeJS.Timeout | undefined, // the timer to show the session extension confirmation modal
endTimeout: undefined as NodeJS.Timeout | undefined // the timer to kill the session due to timeout
endTimeout: undefined as NodeJS.Timeout | undefined, // the timer to kill the session due to timeout
}),
actions: {
// Opens a connection with the web socket
initConnection() {
const socketUrl = `${
import.meta.env.VITE_APP_CURR_ENV ? import.meta.env.VITE_APP_API_URL : 'http://localhost:6040'
}`;
this.socket = new WebSocket(socketUrl);
return new Promise((resolve) => {
const socketUrl = `${
import.meta.env.VITE_APP_CURR_ENV ? import.meta.env.VITE_APP_API_URL : 'http://localhost:6040'
}`;
this.socket = new WebSocket(socketUrl);

// Connection opened
this.socket.onopen = () => {
this.connected = true;
return false;
};
// Connection opened
this.socket.onopen = () => {
this.connected = true;
resolve();
};

// Listen for messages
this.socket.onmessage = (event) => {
const res = JSON.parse(event.data);
this.received = true;
this.result = res;
};
// Listen for messages
this.socket.onmessage = (event) => {
const res = JSON.parse(event.data);
this.received = true;
this.result = res;
};
});
},
// Attempts to lock a storyline for this user.
// Attempts to lock a storyline for this user.
// Returns a promise that resolves if the lock was successfully fetched and rejects if it was not.
lockStoryline(uuid: string): Promise<void> {
// Stop the previous storyline's timer.
async lockStoryline(uuid: string): Promise<void> {
// Stop the previous storyline's timer
clearInterval(this.timeInterval);

// If not connected or socket isn't open, try to connect first
if (!this.connected || !this.socket || this.socket.readyState !== WebSocket.OPEN) {
await this.initConnection();
}

return new Promise((resolve, reject) => {
// First we need to keep polling for the connection to be established.
// Is there a better way to do this? :(
const connectionPoll = setInterval(() => {
if (this.connected) {
// Now that we are connected, we need to poll for the message to be received back from the
// web socket server.
clearInterval(connectionPoll);
this.received = false;
this.socket?.send(JSON.stringify({ uuid, lock: true }));
const receiptPoll = setInterval(() => {
if (this.received) {
clearInterval(receiptPoll);
if (this.result.status === 'fail') {
reject();
} else {
this.uuid = uuid;
this.secret = this.result.secret;
this.broadcast = new BroadcastChannel(this.result.secret);
resolve();
}
}
});
this.received = false;
this.socket?.send(JSON.stringify({ uuid, lock: true }));

const handleMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data);

if(data !== undefined){
if(data.status === 'fail'){
this.socket!.removeEventListener('message', handleMessage);
reject(new Error(data.message || 'Failed to lock storyline.'));
}
else if (data.status === 'success') {
this.socket!.removeEventListener('message', handleMessage);

this.uuid = uuid;
this.secret = data.secret;
this.broadcast = new BroadcastChannel(data.secret);

resolve();
}
}
}, 100);
};

this.socket!.addEventListener('message', handleMessage);
});
},
// Unlocks the curent storyline for this user. Only to be called on session end.
Expand Down

0 comments on commit 0465138

Please sign in to comment.