(core) Add flexibility to daily API usage limit

Summary: Allow exceeding the daily API usage limit for a doc based on additional allocations for the current hour and minute. See the doc comment on getDocApiUsageKeysToIncr for details. This means that up to 5 redis keys may be relevant at a time for a single document.

Test Plan: Updated and expanded 'Daily API Limit' tests.

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D3368
This commit is contained in:
Alex Hall
2022-04-28 13:51:55 +02:00
parent 4de5928396
commit 0beb2898cb
10 changed files with 347 additions and 107 deletions

View File

@@ -43,6 +43,11 @@ export const teamFreeFeatures: Features = {
gracePeriodDays: 14,
};
export const testDailyApiLimitFeatures = {
...teamFreeFeatures,
baseMaxApiUnitsPerDocumentPerDay: 3,
};
/**
* A summary of features used in unrestricted grandfathered accounts, and also
* in some test settings.
@@ -87,7 +92,7 @@ export interface IProduct {
* TODO: change capitalization of name of grandfather product.
*
*/
const PRODUCTS: IProduct[] = [
export const PRODUCTS: IProduct[] = [
// This is a product for grandfathered accounts/orgs.
{
name: 'Free',
@@ -166,7 +171,9 @@ export class Product extends BaseEntity {
* If `apply` is set, the products are changed in the db, otherwise
* the are left unchanged. A summary of affected products is returned.
*/
export async function synchronizeProducts(connection: Connection, apply: boolean): Promise<string[]> {
export async function synchronizeProducts(
connection: Connection, apply: boolean, products = PRODUCTS
): Promise<string[]> {
try {
await connection.query('select name, features, stripe_product_id from products limit 1');
} catch (e) {
@@ -175,7 +182,7 @@ export async function synchronizeProducts(connection: Connection, apply: boolean
}
const changingProducts: string[] = [];
await connection.transaction(async transaction => {
const desiredProducts = new Map(PRODUCTS.map(p => [p.name, p]));
const desiredProducts = new Map(products.map(p => [p.name, p]));
const existingProducts = new Map((await transaction.find(Product))
.map(p => [p.name, p]));
for (const product of desiredProducts.values()) {

View File

@@ -137,8 +137,8 @@ class DummyDocWorkerMap implements IDocWorkerMap {
return null;
}
public incrementDocApiUsage(key: string): Promise<number> {
return Promise.resolve(0);
public getRedisClient(): RedisClient {
throw new Error("No redis client here");
}
}
@@ -517,16 +517,8 @@ export class DocWorkerMap implements IDocWorkerMap {
return this._client.getAsync(`doc-${docId}-group`);
}
/**
* Increment the value at the given redis key representing API usage of one document in one day.
* Expire the key after a day just so that it cleans itself up.
* Returns the value after incrementing.
* This is not related to other responsibilities of this class,
* but this class conveniently manages the redis client.
*/
public async incrementDocApiUsage(key: string): Promise<number | null> {
const result = await this._client.multi().incr(key).expire(key, 24 * 60 * 60).execAsync();
return Number(result?.[0]);
public getRedisClient(): RedisClient {
return this._client;
}
/**