Skip to content

Commit 4ea6d98

Browse files
committed
fix(notifications): extension activation hangs if fetch fails
Problem: If notifications fail to fetch, it will retry multiple times with 30 second waits. Activation is awaiting on this though, so it may take forever to finish. Solution: don't await on notification activation Also, update logging statements.
1 parent 7389d24 commit 4ea6d98

File tree

4 files changed

+47
-37
lines changed

4 files changed

+47
-37
lines changed

packages/amazonq/src/extensionNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) {
7575
...authState,
7676
})
7777

78-
await activateNotifications(context, authState, getAuthState)
78+
void activateNotifications(context, authState, getAuthState)
7979
}
8080

8181
async function getAuthState(): Promise<Omit<AuthUserState, 'source'>> {

packages/core/src/extensionNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ export async function activate(context: vscode.ExtensionContext) {
246246
...authState,
247247
})
248248

249-
await activateNotifications(context, authState, getAuthState)
249+
void activateNotifications(context, authState, getAuthState)
250250
} catch (error) {
251251
const stacktrace = (error as Error).stack?.split('\n')
252252
// truncate if the stacktrace is unusually long

packages/core/src/notifications/activation.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { AuthState } from './types'
1313
import { getLogger } from '../shared/logger/logger'
1414
import { oneMinute } from '../shared/datetime'
1515

16+
const logger = getLogger('notifications')
17+
1618
/** Time in MS to poll for emergency notifications */
1719
const emergencyPollTime = oneMinute * 10
1820

@@ -33,19 +35,23 @@ export async function activate(
3335
return
3436
}
3537

36-
const panelNode = NotificationsNode.instance
37-
panelNode.registerView(context)
38+
try {
39+
const panelNode = NotificationsNode.instance
40+
panelNode.registerView(context)
3841

39-
const controller = new NotificationsController(panelNode)
40-
const engine = new RuleEngine(await getRuleContext(context, initialState))
42+
const controller = new NotificationsController(panelNode)
43+
const engine = new RuleEngine(await getRuleContext(context, initialState))
4144

42-
await controller.pollForStartUp(engine)
43-
await controller.pollForEmergencies(engine)
45+
await controller.pollForStartUp(engine)
46+
await controller.pollForEmergencies(engine)
4447

45-
globals.clock.setInterval(async () => {
46-
const ruleContext = await getRuleContext(context, await authStateFn())
47-
await controller.pollForEmergencies(new RuleEngine(ruleContext))
48-
}, emergencyPollTime)
48+
globals.clock.setInterval(async () => {
49+
const ruleContext = await getRuleContext(context, await authStateFn())
50+
await controller.pollForEmergencies(new RuleEngine(ruleContext))
51+
}, emergencyPollTime)
4952

50-
getLogger('notifications').debug('Activated in-IDE notifications polling module')
53+
logger.debug('Activated in-IDE notifications polling module')
54+
} catch (err) {
55+
logger.error('Failed to activate in-IDE notifications module.')
56+
}
5157
}

packages/core/src/notifications/controller.ts

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import { FileResourceFetcher } from '../shared/resourcefetcher/fileResourceFetch
2626
import { isAmazonQ } from '../shared/extensionUtilities'
2727
import { telemetry } from '../shared/telemetry/telemetry'
2828

29+
const logger = getLogger('notifications')
30+
2931
/**
3032
* Handles fetching and maintaining the state of in-IDE notifications.
3133
* Notifications are constantly polled from a known endpoint and then stored in global state.
@@ -39,8 +41,6 @@ import { telemetry } from '../shared/telemetry/telemetry'
3941
* Emergency notifications - fetched at a regular interval.
4042
*/
4143
export class NotificationsController {
42-
public static readonly suggestedPollIntervalMs = 1000 * 60 * 10 // 10 minutes
43-
4444
/** Internal memory state that is written to global state upon modification. */
4545
private readonly state: NotificationsState
4646

@@ -75,7 +75,7 @@ export class NotificationsController {
7575
this.readState()
7676
await this.fetchNotifications(category)
7777
} catch (err: any) {
78-
getLogger('notifications').error(`Unable to fetch %s notifications: %s`, category, err)
78+
logger.error(`Unable to fetch %s notifications: %s`, category, err)
7979
}
8080

8181
await this.displayNotifications(ruleEngine)
@@ -121,7 +121,9 @@ export class NotificationsController {
121121
* hide all notifications.
122122
*/
123123
public async dismissNotification(notificationId: string) {
124-
getLogger('notifications').debug('Dismissing notification: %s', notificationId)
124+
logger.debug('Dismissing notification: %s', notificationId)
125+
126+
this.readState() // Don't overwrite dismissals from other windows
125127
this.state.dismissed.push(notificationId)
126128
await this.writeState()
127129

@@ -134,7 +136,7 @@ export class NotificationsController {
134136
private async fetchNotifications(category: NotificationType) {
135137
const response = await this.fetcher.fetch(category, this.state[category].eTag)
136138
if (!response.content) {
137-
getLogger('notifications').verbose('No new notifications for category: %s', category)
139+
logger.verbose('No new notifications for category: %s', category)
138140
return
139141
}
140142
// Parse the notifications
@@ -149,7 +151,7 @@ export class NotificationsController {
149151
const addedNotifications = newNotifications.filter((n: any) => !currentNotificationIds.has(n.id))
150152

151153
if (addedNotifications.length > 0) {
152-
getLogger('notifications').verbose(
154+
logger.verbose(
153155
'New notifications received for category %s, ids: %s',
154156
category,
155157
addedNotifications.map((n: any) => n.id).join(', ')
@@ -161,7 +163,7 @@ export class NotificationsController {
161163
this.state[category].eTag = response.eTag
162164
await this.writeState()
163165

164-
getLogger('notifications').verbose(
166+
logger.verbose(
165167
"Fetched notifications JSON for category '%s' with schema version: %s. There were %d notifications.",
166168
category,
167169
this.state[category].payload?.schemaVersion,
@@ -173,7 +175,7 @@ export class NotificationsController {
173175
* Write the latest memory state to global state.
174176
*/
175177
private async writeState() {
176-
getLogger('notifications').debug('NotificationsController: Updating notifications state at %s', this.storageKey)
178+
logger.debug('NotificationsController: Updating notifications state at %s', this.storageKey)
177179

178180
// Clean out anything in 'dismissed' that doesn't exist anymore.
179181
const notifications = new Set(
@@ -239,7 +241,7 @@ function registerDismissCommand() {
239241
await NotificationsController.instance.dismissNotification(notification.id)
240242
})
241243
} else {
242-
getLogger('notifications').error(`${name}: Cannot dismiss notification: item is not a vscode.TreeItem`)
244+
logger.error(`${name}: Cannot dismiss notification: item is not a vscode.TreeItem`)
243245
}
244246
})
245247
)
@@ -277,17 +279,23 @@ export class RemoteFetcher implements NotificationFetcher {
277279
const fetcher = new HttpResourceFetcher(endpoint, {
278280
showUrl: true,
279281
})
280-
getLogger('notifications').verbose(
281-
'Attempting to fetch notifications for category: %s at endpoint: %s',
282-
category,
283-
endpoint
282+
logger.verbose('Attempting to fetch notifications for category: %s at endpoint: %s', category, endpoint)
283+
284+
return withRetries(
285+
async () => {
286+
try {
287+
return await fetcher.getNewETagContent(versionTag)
288+
} catch (err) {
289+
logger.error('Failed to fetch at endpoint: %s, err: %s', endpoint, err)
290+
throw err
291+
}
292+
},
293+
{
294+
maxRetries: RemoteFetcher.retryNumber,
295+
delay: RemoteFetcher.retryIntervalMs,
296+
// No exponential backoff - necessary?
297+
}
284298
)
285-
286-
return withRetries(async () => await fetcher.getNewETagContent(versionTag), {
287-
maxRetries: RemoteFetcher.retryNumber,
288-
delay: RemoteFetcher.retryIntervalMs,
289-
// No exponential backoff - necessary?
290-
})
291299
}
292300
}
293301

@@ -309,11 +317,7 @@ export class LocalFetcher implements NotificationFetcher {
309317

310318
async fetch(category: NotificationType, versionTag?: string): Promise<ResourceResponse> {
311319
const uri = category === 'startUp' ? this.startUpLocalPath : this.emergencyLocalPath
312-
getLogger('notifications').verbose(
313-
'Attempting to fetch notifications locally for category: %s at path: %s',
314-
category,
315-
uri
316-
)
320+
logger.verbose('Attempting to fetch notifications locally for category: %s at path: %s', category, uri)
317321

318322
return {
319323
content: await new FileResourceFetcher(globals.context.asAbsolutePath(uri)).get(),

0 commit comments

Comments
 (0)