Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic TTL #312

Merged
merged 6 commits into from
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 62 additions & 76 deletions web_services/profile/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const NodeCache = require("node-cache");
const config = require("./config");
const { renderStats } = require("./stats");
const bn = require("bignum");
const {TTLExtension, channel_expires_header} = require('./config')

const dataCache = new NodeCache(config.data_cache_config);
const channelCache = new NodeCache(config.channel_config);
Expand All @@ -21,6 +22,19 @@ if (config.is_dev) {
});
}

/* Get remaining time to live of channel in seconds */
const getRemainingTTL = (channelId) => {
// NodeCache.getTtl() actually returns a unix timestamp in ms(!) when channel will expire
const expirationTime = channelCache.getTtl(channelId);
const remainingTTL = expirationTime - Date.now();
return Math.floor(remainingTTL/1000)
}

/* Get expiration timestamp of channel as unix timestamp(seconds since 1970) */
const getExpirationTimestamp = (channelId) => {
return Math.floor(channelCache.getTtl(channelId)/1000)
}

app.get("/", function (req, res, next) {
res.send("BrightID socket server");
});
Expand Down Expand Up @@ -56,7 +70,8 @@ app.post("/upload/:channelId", function (req, res) {
const ttl = requestedTtl || config.defaultTTL;

let channel = channelCache.get(channelId);
if (!channel) {
const channelExisting = !!channel
if (!channelExisting) {
// Create new channel.
channel = {
entries: new Map(),
Expand All @@ -66,57 +81,57 @@ app.post("/upload/:channelId", function (req, res) {
// save channel in cache with requested TTL
channelCache.set(channelId, channel, ttl);
console.log(`Created new channel ${channelId} with TTL ${channel.ttl}`);
} else {
// existing channel. check if this channel was about to expire, but got another upload
if (channel.entries.size === 0) {
console.log(
`Restoring requested TTL ${channel.ttl} for channel ${channelId}`
);
channelCache.ttl(channelId, channel.ttl);
}
}

// Check if there is already data with the provided uuid to prevent duplicates
const existingData = channel.entries.get(uuid);
if (existingData) {
if (existingData === data) {
console.log(
`Received duplicate profile ${uuid} for channel ${channelId}`
);
// Workaround for recovery channels: interpret upload of existing data as request to extend TTL of channel
// TODO: Remove ttl extension when client that knows how to create channels with longer ttl time is released
channelCache.ttl(channelId, channel.ttl);
res.status(201).json({ success: true });
} else {
if (existingData !== data) {
// Same UUID but different content? This is scary. Likely client bug. Bail out.
res
.status(500)
.json({
error: `Profile ${uuid} already exists in channel ${channelId} with different data.`,
});
.status(500)
.json({
error: `Profile ${uuid} already exists in channel ${channelId} with different data.`,
})
return;
}
return;
}

// check channel size
const entrySize = sizeof(data) + sizeof(uuid);
const newSize = channel.size + entrySize;
console.log(
`channel ${channelId} newSize: ${newSize},\t delta: ${entrySize} bytes`
);
if (newSize > config.channel_max_size_bytes) {
// channel full :-(
res
.status(config.channel_limit_response_code)
.json({ error: config.channel_limit_message });
return;
console.log(
`Received duplicate profile ${uuid} for channel ${channelId}`,
)
}

// save data in cache
try {
channel.entries.set(uuid, data);
channel.size = newSize;
if (!existingData) {
// check channel size
const entrySize = sizeof(data) + sizeof(uuid);
const newSize = channel.size + entrySize;
console.log(
`channel ${channelId} newSize: ${newSize},\t delta: ${entrySize} bytes`
);
if (newSize > config.channel_max_size_bytes) {
// channel full :-(
res
.status(config.channel_limit_response_code)
.json({ error: config.channel_limit_message });
return;
}

// save new data
channel.entries.set(uuid, data);
channel.size = newSize;
}

// extend channel TTL if necessary
if (channelExisting) {
const remainingTTL = getRemainingTTL(channelId)
if ( remainingTTL < TTLExtension) {
channelCache.ttl(channelId, TTLExtension)
console.log(`Extending TTL of channel ${channelId}. Old: ${remainingTTL} New: ${getRemainingTTL(channelId)}`)
}
}

res.status(201);
res.append(channel_expires_header, `${getExpirationTimestamp(channelId)}`)
res.json({ success: true });
} catch (e) {
console.log(err);
Expand Down Expand Up @@ -153,6 +168,8 @@ app.get("/download/:channelId/:uuid", function (req, res, next) {
return;
}

res.append(channel_expires_header, `${getExpirationTimestamp(channelId)}`)

res.json({
data: data,
});
Expand Down Expand Up @@ -201,40 +218,11 @@ app.delete("/:channelId/:uuid", function (req, res, next) {

// update channel size
channel.size -= sizeof(data) + sizeof(uuid);

console.log(
`Deleted ${uuid} from channel ${channelId}. New size: ${channel.size}`
);

// handle removing of last entry
if (channel.entries.size === 0) {
// if channel is empty size should also be 0. Double-check.
if (channel.size !== 0) {
console.warn(
`Channel size calculation incorrect. This should not happen.`
);
channel.size = 0;
}

// Reduce remaining TTL. Leave a few minutes TTL in case some upload is
// hanging from a slow connection
const expirationTime = channelCache.getTtl(channelId); // This actually returns a unix timestamp in ms(!) when channel will expire
const remainingTTL = expirationTime - Date.now();
if (remainingTTL > config.finalTTL) {
console.log(
`last element removed from channel ${channelId}. Reducing TTL from ${Math.floor(
remainingTTL / 1000
)} to ${config.finalTTL} secs.`
);
channelCache.ttl(channelId, config.finalTTL);
} else {
console.log(
`last element removed from channel ${channelId}. Remaining TTL: ${remainingTTL}ms.`
);
channelCache.ttl(channelId, config.finalTTL);
}
}

res.append(channel_expires_header, `${getExpirationTimestamp(channelId)}`)
res.status(200);
res.json({ success: true });
});
Expand All @@ -250,11 +238,7 @@ app.get("/list/:channelId", function (req, res, next) {
// get channel
const channel = channelCache.get(channelId);
if (!channel) {
// Don't fail when channel is not existing. Instead return empty array
// res.status(404).json({error: `Channel ${channelId} not found`});
res.json({
profileIds: [],
});
res.status(404).json({error: `channelId ${channelId} not found`})
return;
}

Expand All @@ -265,8 +249,10 @@ app.get("/list/:channelId", function (req, res, next) {
return;
}

res.append(channel_expires_header, `${getExpirationTimestamp(channelId)}`)

res.json({
profileIds: Array.from(channel.entries.keys()), // channel.entries.keys()
profileIds: Array.from(channel.entries.keys()),
});
});

Expand Down
9 changes: 4 additions & 5 deletions web_services/profile/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const port = process.env.BN_WS_PROFILE_SERVICE_PORT || 3000;
const minTTL = 60; // 1 minute
const maxTTL = 60 * 60 * 24; // 24 hours
const defaultTTL = 60 * 15; // 15 minutes
const finalTTL = 600; // 10 minutes grace period to keep empty channels open
const TTLExtension = 600;

/* Cache config for channels */
const channel_config = {
Expand All @@ -29,27 +29,26 @@ const notification_service =
? process.env.NOTIFICATION_SERVICE_DEV
: process.env.NOTIFICATION_SERVICE_RELEASE;

const channel_entry_limit = 30;

const channel_max_size_bytes = is_test
? 1024 // 1 kb when running jest tests
: 1024 * 1024 * 20; // 20 MegaByte normally

const channel_limit_response_code = 440;
const channel_limit_message = "Channel full";
const channel_expires_header = "x-expires";

module.exports = {
is_dev,
port,
channel_config,
data_cache_config,
notification_service,
channel_entry_limit,
channel_max_size_bytes,
channel_limit_response_code,
channel_limit_message,
finalTTL,
channel_expires_header,
minTTL,
maxTTL,
defaultTTL,
TTLExtension,
};
Loading