(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
pull/214/head
Alex Hall 2 years ago
parent b57a211741
commit 0005ad013e

@ -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();

@ -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>;

Loading…
Cancel
Save