Skip to content
Closed
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
1 change: 1 addition & 0 deletions lib/utils/hasValue.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/**
* Returns true if specified value is not undefined, null and empty string
*
* @param v - value to check
*/
export function hasValue<T>(v: T | undefined | null): v is T {
Expand Down
17 changes: 0 additions & 17 deletions workers/limiter/src/dbHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,23 +109,6 @@ export class DbHelper {
await this.workspacesCollection.bulkWrite(operations);
}

/**
* Method to change workspace isBlocked state
*
* @param workspaceId - id of the workspace to be changed
* @param isBlocked - new isBlocked state of the workspace
*/
public async changeWorkspaceBlockedState(workspaceId: string, isBlocked: boolean): Promise<void> {
await this.workspacesCollection.updateOne(
{ _id: new ObjectId(workspaceId) },
{
$set: {
isBlocked,
},
}
);
}

/**
* Returns total event counts for last billing period
*
Expand Down
32 changes: 27 additions & 5 deletions workers/limiter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export default class LimiterWorker extends Worker {
* @param event - event to handle
*/
private async handleBlockWorkspaceEvent(event: BlockWorkspaceEvent): Promise<void> {
this.logger.info('handle block workspace event', event);

const workspace = await this.dbHelper.getWorkspacesWithTariffPlans(event.workspaceId);

if (!workspace) {
Expand All @@ -126,7 +128,13 @@ export default class LimiterWorker extends Worker {
const workspaceProjects = await this.dbHelper.getProjects(event.workspaceId);
const projectIds = workspaceProjects.map(project => project._id.toString());

await this.dbHelper.changeWorkspaceBlockedState(event.workspaceId, true);
const { updatedWorkspace } = await this.prepareWorkspaceUsageUpdate(workspace, workspaceProjects);

updatedWorkspace.isBlocked = true;
await this.dbHelper.updateWorkspacesEventsCountAndIsBlocked([updatedWorkspace]);

this.logger.info('workspace blocked in db ', event.workspaceId)

await this.redis.appendBannedProjects(projectIds);

this.sendSingleWorkspaceReport(workspaceProjects, workspace, 'blocked');
Expand Down Expand Up @@ -159,16 +167,18 @@ export default class LimiterWorker extends Worker {
/**
* If workspace should be blocked by quota - then do not unblock it
*/
const { shouldBeBlockedByQuota } = await this.prepareWorkspaceUsageUpdate(workspace, workspaceProjects);
const { shouldBeBlockedByQuota, updatedWorkspace } = await this.prepareWorkspaceUsageUpdate(workspace, workspaceProjects);

if (shouldBeBlockedByQuota) {
return;
}

await this.dbHelper.changeWorkspaceBlockedState(event.workspaceId, false);
updatedWorkspace.isBlocked = false;

await this.dbHelper.updateWorkspacesEventsCountAndIsBlocked([updatedWorkspace]);
await this.redis.removeBannedProjects(projectIds);

this.sendSingleWorkspaceReport(workspaceProjects, workspace, 'unblocked');
this.sendSingleWorkspaceReport(workspaceProjects, updatedWorkspace, 'unblocked');
}

/**
Expand All @@ -182,6 +192,13 @@ export default class LimiterWorker extends Worker {
const updatedWorkspaces: WorkspaceWithTariffPlan[] = [];

await Promise.all(workspaces.map(async (workspace) => {
/**
* If workspace is already blocked - do nothing
*/
if (workspace.isBlocked) {
return;
}

const workspaceProjects = await this.dbHelper.getProjects(workspace._id.toString());

const { shouldBeBlockedByQuota, updatedWorkspace, projectsToUpdate } = await this.prepareWorkspaceUsageUpdate(workspace, workspaceProjects);
Expand All @@ -198,7 +215,7 @@ export default class LimiterWorker extends Worker {
/**
* If workspace is not blocked yet and it should be blocked by quota - then block it
*/
if (!workspace.isBlocked && shouldBeBlockedByQuota) {
if (shouldBeBlockedByQuota) {
const projectIds = projectsToUpdate.map(project => project._id.toString());

this.redis.appendBannedProjects(projectIds);
Expand All @@ -222,6 +239,8 @@ export default class LimiterWorker extends Worker {
private async prepareWorkspaceUsageUpdate(
workspace: WorkspaceWithTariffPlan, projects: ProjectDBScheme[]
): Promise<WorkspaceReport> {
this.logger.info('prepareWorkspaceUsageUpdate');

/**
* If last charge date is not specified, then we skip checking it
* In the next time the Paymaster worker starts, it will set lastChargeDate for this workspace
Expand All @@ -240,6 +259,9 @@ export default class LimiterWorker extends Worker {
const since = Math.floor(new Date(workspace.lastChargeDate).getTime() / MS_IN_SEC);

const workspaceEventsCount = await this.dbHelper.getEventsCountByProjects(projects, since);

this.logger.info(`workspace ${workspace._id} events count since last charge date: ${workspaceEventsCount}`);

const usedQuota = workspaceEventsCount / workspace.tariffPlan.eventsLimit;
const quotaNotification = NOTIFY_ABOUT_LIMIT.reverse().find(quota => quota < usedQuota);

Expand Down
28 changes: 0 additions & 28 deletions workers/limiter/tests/dbHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,34 +272,6 @@ describe('DbHelper', () => {
});
});

describe('changeWorkspaceBlockedState', () => {
test('Should change workspace blocked state', async () => {
/**
* Arrange
*/
const workspace = createWorkspaceMock({
plan: mockedPlans.eventsLimit10,
billingPeriodEventsCount: 0,
lastChargeDate: new Date(),
isBlocked: false,
});

await workspaceCollection.insertOne(workspace);

/**
* Act
*/
await dbHelper.changeWorkspaceBlockedState(workspace._id.toString(), true);

/**
* Assert
*/
const updatedWorkspace = await workspaceCollection.findOne({ _id: workspace._id });

expect(updatedWorkspace.isBlocked).toBe(true);
});
});

describe('getEventsCountByProject', () => {
test('Should count events and repetitions for a project', async () => {
/**
Expand Down
61 changes: 50 additions & 11 deletions workers/paymaster/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,24 +60,47 @@ export default class PaymasterWorker extends Worker {
/**
* Check if today is a payday for passed timestamp
*
* Pay day is calculated by formula: last charge date + 30 days
*
* @param date - last charge date
* @param paidUntil - paid until date
* @param isDebug - flag for debug purposes
*/
private static isTimeToPay(date: Date, paidUntil: Date, isDebug = false): boolean {
const expectedPayDay = paidUntil ? new Date(paidUntil) : new Date(date);
private static isTimeToPay(date: Date, paidUntil?: Date, isDebug = false): boolean {
const expectedPayDay = paidUntil ? new Date(paidUntil) : this.composeBillingPeriodEndDate(date, isDebug);
const now = new Date();

return now >= expectedPayDay;
}

/**
* Check if today is a recharge day for passed timestamp
* It equals to the isTimeToPay in all cases except prePaid workspaces
*
* @param date - last charge date
* @param isDebug - flag for debug purposes
*/
private static isTimeToRecharge(date: Date, isDebug = false): boolean {
const nexTimeToRecharge = this.composeBillingPeriodEndDate(date, isDebug);
const now = new Date();

return now >= nexTimeToRecharge;
}

/**
* Returns the date - end of the billing period
*
* @param date - last charge date
* @param isDebug - flag for debug workspaces
*/
private static composeBillingPeriodEndDate(date: Date, isDebug = false): Date {
const endDate = new Date(date);

if (isDebug) {
expectedPayDay.setDate(date.getDate() + 1);
} else if (!paidUntil) {
expectedPayDay.setMonth(date.getMonth() + 1);
endDate.setDate(date.getDate() + 1);
} else {
endDate.setMonth(date.getMonth() + 1);
}

const now = new Date().getTime();

return now >= expectedPayDay.getTime();
return endDate;
}

/**
Expand Down Expand Up @@ -214,6 +237,12 @@ export default class PaymasterWorker extends Worker {
// @ts-expect-error debug
const isTimeToPay = PaymasterWorker.isTimeToPay(workspace.lastChargeDate, workspace.paidUntil, workspace.isDebug);

/**
* Is it time to recharge workspace limits
*/
// @ts-expect-error debug
const isTimeToRecharge = PaymasterWorker.isTimeToRecharge(workspace.lastChargeDate, workspace.isDebug);

/**
* How many days have passed since payments the expected day of payments
*/
Expand All @@ -237,7 +266,17 @@ export default class PaymasterWorker extends Worker {
*/
if (!isTimeToPay) {
/**
* If workspace was manually unblocked (reset of billingPeriodEventsCount and lastChargeDate) in db
* If it is time to recharge workspace limits, but not time to pay
* Start new month - recharge billing period events count and update last charge date
*/
if (isTimeToRecharge) {
await this.updateLastChargeDate(workspace, date);
await this.clearBillingPeriodEventsCount(workspace);
}

/**
* If workspace is blocked, but it is not time to pay
* This case could be reached by prepaid workspaces and manually recharged ones
*/
if (workspace.isBlocked) {
await this.unblockWorkspace(workspace);
Expand Down
63 changes: 58 additions & 5 deletions workers/paymaster/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,11 +481,6 @@ describe('PaymasterWorker', () => {
MockDate.reset();
});

afterAll(async () => {
await connection.close();
MockDate.reset();
});

test('Should send notification if payday is coming for workspace with paidUntil value', async () => {
/**
* Arrange
Expand Down Expand Up @@ -542,4 +537,62 @@ describe('PaymasterWorker', () => {

MockDate.reset();
});

test('Should recharge workspace billing period when month passes since last charge date and paidUntil is set to several months in the future', async () => {
/**
* Arrange
*/
const currentDate = new Date();
const lastChargeDate = new Date(currentDate.getTime());

lastChargeDate.setMonth(lastChargeDate.getMonth() - 1); // Set last charge date to 1 month ago

const paidUntil = new Date(currentDate.getTime());

paidUntil.setMonth(paidUntil.getMonth() + 3); // Set paidUntil to 3 months in the future

const plan = createPlanMock({
monthlyCharge: 100,
isDefault: true,
});
const workspace = createWorkspaceMock({
plan,
subscriptionId: null,
lastChargeDate,
isBlocked: false,
billingPeriodEventsCount: 10,
paidUntil,
});

await fillDatabaseWithMockedData({
workspace,
plan,
});

MockDate.set(currentDate);

/**
* Act
*/
const worker = new PaymasterWorker();

await worker.start();
await worker.handle(WORKSPACE_SUBSCRIPTION_CHECK);
await worker.finish();

/**
* Assert
*/
const updatedWorkspace = await workspacesCollection.findOne({ _id: workspace._id });

expect(updatedWorkspace.lastChargeDate).toEqual(currentDate);
expect(updatedWorkspace.billingPeriodEventsCount).toEqual(0);

MockDate.reset();
});

afterAll(async () => {
await connection.close();
MockDate.reset();
});
});
14 changes: 12 additions & 2 deletions workers/release/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,17 +179,23 @@ export default class ReleaseWorker extends Worker {
const fileInfo = await this.saveFile(map);

/**
* Remove 'content' and save id of saved file instead
* Save id of saved file instead
*/
map._id = fileInfo._id;
delete map.content;

return map;
} catch (error) {
this.logger.error(`Map ${map.mapFileName} was not saved: ${error}`);
}
}));

/**
* Delete file content after it is saved to the GridFS
*/
savedFiles.forEach(file => {
delete file.content;
})

/**
* Filter unsaved maps
*/
Expand Down Expand Up @@ -282,6 +288,10 @@ export default class ReleaseWorker extends Worker {
*/
private saveFile(file: SourceMapDataExtended): Promise<SourceMapFileChunk> {
return new Promise((resolve, reject) => {
if (!file.content) {
return reject(new Error('Source map content is empty'));
}

const readable = Readable.from([ file.content ]);
const writeStream = this.db.getBucket().openUploadStream(file.mapFileName);

Expand Down
Loading