(core) Notify open ActiveDocs when the product is upgraded

Summary:
When an account is upgraded to a new product in Billing, send a message to the redis channel `billingAccount-${accountId}-product-changed`.

ActiveDocs subscribe to this channel. When a message is received, they refresh their product from the database and use it to recalculate doc usage based on new limits. The new usage is broadcast to clients so they see the result of the upgrade live.

Test Plan: Extended nbrowser Billing test to test that a document open in a separate tab has its limit banner cleared immediately on upgrade.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3480
This commit is contained in:
Alex Hall 2022-06-14 16:12:46 +02:00
parent b57a211741
commit 0005ad013e
2 changed files with 24 additions and 1 deletions

View File

@ -87,6 +87,7 @@ import {IMessage, MsgType} from 'grain-rpc';
import * as imageSize from 'image-size';
import * as moment from 'moment-timezone';
import fetch from 'node-fetch';
import {createClient, RedisClient} from 'redis';
import * as tmp from 'tmp';
import {ActionHistory} from './ActionHistory';
@ -200,6 +201,9 @@ export class ActiveDoc extends EventEmitter {
private _gracePeriodStart: Date|null = null;
private _isForkOrSnapshot: boolean = false;
// Client watching for 'product changed' event published by Billing to update usage
private _redisSubscriber?: RedisClient;
// Timer for shutting down the ActiveDoc a bit after all clients are gone.
private _inactivityTimer = new InactivityTimer(() => this.shutdown(), Deps.ACTIVEDOC_TIMEOUT * 1000);
private _recoveryMode: boolean = false;
@ -230,9 +234,21 @@ export class ActiveDoc extends EventEmitter {
if (_options?.safeMode) { this._recoveryMode = true; }
if (_options?.doc) {
const {gracePeriodStart, workspace, usage} = _options.doc;
this._product = workspace.org.billingAccount?.product;
const billingAccount = workspace.org.billingAccount;
this._product = billingAccount?.product;
this._gracePeriodStart = gracePeriodStart;
if (process.env.REDIS_URL && billingAccount) {
const channel = `billingAccount-${billingAccount.id}-product-changed`;
this._redisSubscriber = createClient(process.env.REDIS_URL);
this._redisSubscriber.subscribe(channel);
this._redisSubscriber.on("message", async () => {
// A product change has just happened in Billing.
// Reload the doc (causing connected clients to reload) to ensure everyone sees the effect of the change.
await this.reloadDoc();
});
}
if (!this._isForkOrSnapshot) {
/* Note: We don't currently persist usage for forks or snapshots anywhere, so
* we need to hold off on setting _docUsage here. Normally, usage is set shortly
@ -452,6 +468,9 @@ export class ActiveDoc extends EventEmitter {
this._triggers.shutdown();
this._redisSubscriber?.quitAsync()
.catch(e => this._log.warn(docSession, "Failed to quit redis subscriber", e));
// Clear the MapWithTTL to remove all timers from the event loop.
this._fetchCache.clear();

View File

@ -30,6 +30,10 @@ declare module "redis" {
class RedisClient {
public eval(args: any[], callback?: (err: Error | null, res: any) => void): any;
public subscribe(channel: string): void;
public on(eventType: string, callback: (...args: any[]) => void): void;
public publishAsync(channel: string, message: string): Promise<number>;
public delAsync(key: string): Promise<'OK'>;
public flushdbAsync(): Promise<void>;
public getAsync(key: string): Promise<string|null>;