Skip to content

Commit 872b62e

Browse files
committed
Update notifications on updates to subscription
1 parent d2064a8 commit 872b62e

File tree

6 files changed

+133
-12
lines changed

6 files changed

+133
-12
lines changed

components/dashboard/src/AppNotifications.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,22 @@ export function AppNotifications() {
1919
setNotifications(localState);
2020
return;
2121
}
22-
(async () => {
23-
const serverState = await getGitpodService().server.getNotifications();
24-
setNotifications(serverState);
25-
if (serverState.length > 0) {
26-
setLocalStorageObject(KEY_APP_NOTIFICATIONS, serverState, /* expires in */ 60 /* seconds */);
27-
}
28-
})();
22+
reloadNotifications().catch(console.error);
23+
24+
getGitpodService().registerClient({
25+
onSubscriptionUpdate: () => reloadNotifications().catch(console.error),
26+
});
2927
}, []);
3028

29+
const reloadNotifications = async () => {
30+
const serverState = await getGitpodService().server.getNotifications();
31+
setNotifications(serverState);
32+
removeLocalStorageObject(KEY_APP_NOTIFICATIONS);
33+
if (serverState.length > 0) {
34+
setLocalStorageObject(KEY_APP_NOTIFICATIONS, serverState, /* expires in */ 300 /* seconds */);
35+
}
36+
};
37+
3138
const topNotification = notifications[0];
3239
if (topNotification === undefined) {
3340
return null;

components/gitpod-messagebus/src/messagebus.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export interface MessageBusHelper {
6161
* @param topic the topic to parse
6262
*/
6363
getWsInformationFromTopic(topic: string): WorkspaceTopic | undefined;
64+
65+
getSubscriptionUpdateTopic(attributionId?: string): string;
6466
}
6567

6668
export const WorkspaceTopic = Symbol("WorkspaceTopic");
@@ -89,6 +91,10 @@ export class MessageBusHelperImpl implements MessageBusHelper {
8991
await ch.assertExchange(this.workspaceExchange, "topic", { durable: true });
9092
}
9193

94+
getSubscriptionUpdateTopic(attributionId: string | undefined): string {
95+
return `subscription.${attributionId || "*"}.update`;
96+
}
97+
9298
/**
9399
* Computes the topic name of for listening to a workspace.
94100
*

components/gitpod-protocol/src/gitpod-service.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export interface GitpodClient {
7070

7171
onPrebuildUpdate(update: PrebuildWithStatus): void;
7272

73+
onSubscriptionUpdate(attributionId: string): void;
74+
7375
onCreditAlert(creditAlert: CreditAlert): void;
7476

7577
//#region propagating reconnection to iframe
@@ -573,6 +575,18 @@ export class GitpodCompositeClient<Client extends GitpodClient> implements Gitpo
573575
}
574576
}
575577
}
578+
579+
onSubscriptionUpdate(attributionId: string): void {
580+
for (const client of this.clients) {
581+
if (client.onSubscriptionUpdate) {
582+
try {
583+
client.onSubscriptionUpdate(attributionId);
584+
} catch (error) {
585+
console.error(error);
586+
}
587+
}
588+
}
589+
}
576590
}
577591

578592
export type GitpodService = GitpodServiceImpl<GitpodClient, GitpodServer>;

components/server/ee/src/workspace/gitpod-server-impl.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
119119
import { BillingModes } from "../billing/billing-mode";
120120
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
121121
import { BillingService } from "../billing/billing-service";
122+
import { MessageBusIntegration } from "../../../src/workspace/messagebus-integration";
122123

123124
@injectable()
124125
export class GitpodServerEEImpl extends GitpodServerImpl {
@@ -167,6 +168,8 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
167168
@inject(BillingModes) protected readonly billingModes: BillingModes;
168169
@inject(BillingService) protected readonly billingService: BillingService;
169170

171+
@inject(MessageBusIntegration) protected readonly messageBus: MessageBusIntegration;
172+
170173
initialize(
171174
client: GitpodClient | undefined,
172175
user: User | undefined,
@@ -179,6 +182,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
179182

180183
this.listenToCreditAlerts();
181184
this.listenForPrebuildUpdates().catch((err) => log.error("error registering for prebuild updates", err));
185+
this.listenForSubscriptionUpdates().catch((err) => log.error("error registering for prebuild updates", err));
182186
}
183187

184188
protected async listenForPrebuildUpdates() {
@@ -206,6 +210,32 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
206210
// TODO(at) we need to keep the list of accessible project up to date
207211
}
208212

213+
protected async listenForSubscriptionUpdates() {
214+
if (!this.user) {
215+
return;
216+
}
217+
const teamIds = (await this.teamDB.findTeamsByUser(this.user.id)).map(({ id }) =>
218+
AttributionId.render({ kind: "team", teamId: id }),
219+
);
220+
for (const attributionId of [AttributionId.render({ kind: "user", userId: this.user.id }), ...teamIds]) {
221+
this.disposables.push(
222+
this.localMessageBroker.listenForSubscriptionUpdates(
223+
attributionId,
224+
(ctx: TraceContext, attributionId: string) =>
225+
TraceContext.withSpan(
226+
"forwardSubscriptionUpdateToClient",
227+
(ctx) => {
228+
traceClientMetadata(ctx, this.clientMetadata);
229+
TraceContext.setJsonRPCMetadata(ctx, "onSubscriptionUpdate");
230+
this.client?.onSubscriptionUpdate(attributionId);
231+
},
232+
ctx,
233+
),
234+
),
235+
);
236+
}
237+
}
238+
209239
protected async getAccessibleProjects() {
210240
if (!this.user) {
211241
return [];
@@ -2104,13 +2134,16 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
21042134
await this.stripeService.setDefaultPaymentMethodForCustomer(customer, setupIntentId);
21052135
await this.stripeService.createSubscriptionForCustomer(customer);
21062136

2107-
const attributionId = AttributionId.render({ kind: "team", teamId });
2137+
const attributionId: AttributionId = { kind: "team", teamId };
2138+
const attributionIdString = AttributionId.render(attributionId);
21082139

21092140
// Creating a cost center for this team
21102141
await this.costCenterDB.storeEntry({
2111-
id: attributionId,
2142+
id: attributionIdString,
21122143
spendingLimit: this.defaultSpendingLimit,
21132144
});
2145+
2146+
this.messageBus.notifyOnSubscriptionUpdate(ctx, attributionId).catch();
21142147
} catch (error) {
21152148
log.error(`Failed to subscribe team '${teamId}' to Stripe`, error);
21162149
throw new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, `Failed to subscribe team '${teamId}' to Stripe`);
@@ -2155,13 +2188,17 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
21552188
if (typeof usageLimit !== "number" || usageLimit < 0) {
21562189
throw new ResponseError(ErrorCodes.BAD_REQUEST, "Unexpected `usageLimit` value.");
21572190
}
2158-
const attributionId = AttributionId.render({ kind: "team", teamId });
2159-
await this.guardCostCenterAccess(ctx, user.id, attributionId, "update");
2191+
2192+
const attributionId: AttributionId = { kind: "team", teamId };
2193+
const attributionIdString = AttributionId.render(attributionId);
2194+
await this.guardCostCenterAccess(ctx, user.id, attributionIdString, "update");
21602195

21612196
await this.costCenterDB.storeEntry({
2162-
id: AttributionId.render({ kind: "team", teamId }),
2197+
id: attributionIdString,
21632198
spendingLimit: usageLimit,
21642199
});
2200+
2201+
this.messageBus.notifyOnSubscriptionUpdate(ctx, attributionId).catch();
21652202
}
21662203

21672204
async getNotifications(ctx: TraceContext): Promise<string[]> {

components/server/src/messaging/local-message-broker.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import { MessageBusIntegration } from "../workspace/messagebus-integration";
2020
export interface PrebuildUpdateListener {
2121
(ctx: TraceContext, evt: PrebuildWithStatus): void;
2222
}
23+
export interface SubscriptionUpdateListener {
24+
(ctx: TraceContext, attributionId: string): void;
25+
}
2326
export interface CreditAlertListener {
2427
(ctx: TraceContext, alert: CreditAlert): void;
2528
}
@@ -38,6 +41,8 @@ export interface LocalMessageBroker {
3841

3942
listenForPrebuildUpdates(projectId: string, listener: PrebuildUpdateListener): Disposable;
4043

44+
listenForSubscriptionUpdates(attributionId: string, listener: SubscriptionUpdateListener): Disposable;
45+
4146
listenToCreditAlerts(userId: string, listener: CreditAlertListener): Disposable;
4247

4348
listenForPrebuildUpdatableEvents(listener: HeadlessWorkspaceEventListener): Disposable;
@@ -69,6 +74,7 @@ export class LocalRabbitMQBackedMessageBroker implements LocalMessageBroker {
6974
protected creditAlertsListeners: Map<string, CreditAlertListener[]> = new Map();
7075
protected headlessWorkspaceEventListeners: Map<string, HeadlessWorkspaceEventListener[]> = new Map();
7176
protected workspaceInstanceUpdateListeners: Map<string, WorkspaceInstanceUpdateListener[]> = new Map();
77+
protected subscriptionUpdateListeners: Map<string, SubscriptionUpdateListener[]> = new Map();
7278

7379
protected readonly disposables = new DisposableCollection();
7480

@@ -151,6 +157,21 @@ export class LocalRabbitMQBackedMessageBroker implements LocalMessageBroker {
151157
},
152158
),
153159
);
160+
this.disposables.push(
161+
this.messageBusIntegration.listenToSubscriptionUpdates((ctx: TraceContext, attributionId: string) => {
162+
TraceContext.setOWI(ctx, {});
163+
164+
const listeners = this.subscriptionUpdateListeners.get(attributionId) || [];
165+
for (const l of listeners) {
166+
try {
167+
l(ctx, attributionId);
168+
} catch (err) {
169+
TraceContext.setError(ctx, err);
170+
log.error("listenForWorkspaceInstanceUpdates", err, { attributionId });
171+
}
172+
}
173+
}),
174+
);
154175
}
155176

156177
async stop() {
@@ -165,6 +186,10 @@ export class LocalRabbitMQBackedMessageBroker implements LocalMessageBroker {
165186
return this.doRegister(userId, listener, this.creditAlertsListeners);
166187
}
167188

189+
listenForSubscriptionUpdates(attributionId: string, listener: SubscriptionUpdateListener): Disposable {
190+
return this.doRegister(attributionId, listener, this.subscriptionUpdateListeners);
191+
}
192+
168193
listenForPrebuildUpdatableEvents(listener: HeadlessWorkspaceEventListener): Disposable {
169194
// we're being cheap here in re-using a map where it just needs to be a plain array.
170195
return this.doRegister(

components/server/src/workspace/messagebus-integration.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as opentracing from "opentracing";
2222
import { CancellationTokenSource } from "vscode-ws-jsonrpc";
2323
import { increaseMessagebusTopicReads } from "../prometheus-metrics";
2424
import { CreditAlert } from "@gitpod/gitpod-protocol/lib/accounting-protocol";
25+
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
2526

2627
interface WorkspaceInstanceUpdateCallback {
2728
(ctx: TraceContext, instance: WorkspaceInstance, ownerId: string | undefined): void;
@@ -72,6 +73,16 @@ export class CreditAlertListener extends AbstractTopicListener<CreditAlert> {
7273
}
7374
}
7475

76+
export class SubscriptionUpdateListener extends AbstractTopicListener<string> {
77+
constructor(protected messageBusHelper: MessageBusHelper, listener: TopicListener<string>) {
78+
super(messageBusHelper.workspaceExchange, listener);
79+
}
80+
81+
topic() {
82+
return this.messageBusHelper.getSubscriptionUpdateTopic();
83+
}
84+
}
85+
7586
export class PrebuildUpdatableQueueListener implements MessagebusListener {
7687
protected channel: Channel | undefined;
7788
protected consumerTag: string | undefined;
@@ -208,6 +219,27 @@ export class MessageBusIntegration extends AbstractMessageBusIntegration {
208219
return Disposable.create(() => cancellationTokenSource.cancel());
209220
}
210221

222+
async notifyOnSubscriptionUpdate(ctx: TraceContext, attributionId: AttributionId) {
223+
if (!this.channel) {
224+
throw new Error("Not connected to message bus");
225+
}
226+
const topic = this.messageBusHelper.getSubscriptionUpdateTopic(AttributionId.render(attributionId));
227+
const msg = Buffer.from(JSON.stringify({}));
228+
await this.messageBusHelper.assertWorkspaceExchange(this.channel);
229+
await super.publish(MessageBusHelperImpl.WORKSPACE_EXCHANGE_LOCAL, topic, msg, {
230+
trace: ctx,
231+
});
232+
}
233+
234+
listenToSubscriptionUpdates(callback: (ctx: TraceContext, attributionId: string) => void): Disposable {
235+
const listener = new SubscriptionUpdateListener(this.messageBusHelper, callback);
236+
const cancellationTokenSource = new CancellationTokenSource();
237+
this.listen(listener, cancellationTokenSource.token).catch((err) => {
238+
/** ignore */
239+
});
240+
return Disposable.create(() => cancellationTokenSource.cancel());
241+
}
242+
211243
async notifyOnPrebuildUpdate(prebuildInfo: PrebuildWithStatus) {
212244
if (!this.channel) {
213245
throw new Error("Not connected to message bus");

0 commit comments

Comments
 (0)