Skip to content

Commit b386001

Browse files
committed
Merge branch 'master' into fixHangingTest
2 parents 2ea8134 + ab0664b commit b386001

File tree

4 files changed

+114
-30
lines changed

4 files changed

+114
-30
lines changed

packages/core/src/amazonq/messages/chatMessageDuration.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,28 @@ export class AmazonQChatMessageDuration {
8383

8484
// TODO: handle onContextCommand round trip time
8585
if (metrics.trigger !== 'onContextCommand') {
86+
const editorReceivedMessage = durationFrom('chatMessageSent', 'editorReceivedMessage')
87+
const featureReceivedMessage = durationFrom('editorReceivedMessage', 'featureReceivedMessage')
88+
const messageDisplayed = durationFrom('featureReceivedMessage', 'messageDisplayed')
89+
let reasonDesc = undefined
90+
91+
/**
92+
* Temporary include more information about outliers so that we can find out if the messages
93+
* aren't being sent or the user is actually doing a different chat flow
94+
*/
95+
if ([editorReceivedMessage, featureReceivedMessage].some((val) => val > 30000 || val < -30000)) {
96+
reasonDesc = JSON.stringify(metrics.events)
97+
}
8698
telemetry.amazonq_chatRoundTrip.emit({
8799
amazonqChatMessageSentTime: metrics.events.chatMessageSent ?? -1,
88-
amazonqEditorReceivedMessageMs: durationFrom('chatMessageSent', 'editorReceivedMessage') ?? -1,
89-
amazonqFeatureReceivedMessageMs:
90-
durationFrom('editorReceivedMessage', 'featureReceivedMessage') ?? -1,
91-
amazonqMessageDisplayedMs: durationFrom('featureReceivedMessage', 'messageDisplayed') ?? -1,
100+
amazonqEditorReceivedMessageMs: editorReceivedMessage ?? -1,
101+
amazonqFeatureReceivedMessageMs: featureReceivedMessage ?? -1,
102+
amazonqMessageDisplayedMs: messageDisplayed ?? -1,
92103
source: metrics.trigger,
93104
duration: totalDuration,
94105
result: 'Succeeded',
95106
traceId: metrics.traceId,
107+
...(reasonDesc !== undefined ? { reasonDesc } : {}),
96108
})
97109
}
98110

packages/core/src/shared/crashMonitoring.ts

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ class Heartbeat {
179179

180180
public async start() {
181181
// Send an initial heartbeat immediately
182-
await withFailCtx('initialSendHeartbeat', () => this.state.sendHeartbeat())
182+
await withFailCtx('sendHeartbeatInitial', () => this.state.sendHeartbeat())
183183

184184
// Send a heartbeat every interval
185185
this.intervalRef = globals.clock.setInterval(async () => {
@@ -200,6 +200,10 @@ class Heartbeat {
200200
this._onFailure.fire()
201201
}
202202
}, this.heartbeatInterval)
203+
204+
// We will know the first heartbeat, and can infer the next ones starting from this timestamp.
205+
// In case of heartbeat failure we have a separate failure metric.
206+
telemetry.ide_heartbeat.emit({ timestamp: globals.clock.Date.now(), id: className, result: 'Succeeded' })
203207
}
204208

205209
/** Stops everything, signifying a graceful shutdown */
@@ -455,38 +459,40 @@ export class FileSystemState {
455459
})
456460
}
457461

458-
await withFailCtx('init', () => fs.mkdir(this.stateDirPath))
462+
await withFailCtx('init', () => nodeFs.mkdir(this.stateDirPath, { recursive: true }))
459463
}
460464

461465
// ------------------ Heartbeat methods ------------------
462466
public async sendHeartbeat() {
463467
const extId = this.extId
468+
const filePath = this.makeStateFilePath(extId)
469+
const now = this.deps.now()
470+
471+
let fileHandle: nodeFs.FileHandle | undefined
464472
try {
465-
const now = this.deps.now()
466473
const func = async () => {
467-
const filePath = this.makeStateFilePath(extId)
468-
469-
// We were seeing weird behavior where we possibly read an old file, even though we overwrote it.
470-
// So this is a sanity check.
471-
await fs.delete(filePath, { force: true })
472-
473-
await fs.writeFile(filePath, JSON.stringify({ ...this.ext, lastHeartbeat: now }, undefined, 4))
474-
475-
// Sanity check to verify the write is accessible immediately after
476-
const heartbeatData = JSON.parse(await fs.readFileText(filePath)) as ExtInstanceHeartbeat
474+
fileHandle = await nodeFs.open(filePath, 'w')
475+
await fileHandle.writeFile(JSON.stringify({ ...this.ext, lastHeartbeat: now }, undefined, 4))
476+
// Noticing that some file reads are not immediately available after write. `fsync` is known to address this.
477+
await fileHandle.sync()
478+
await fileHandle.close()
479+
fileHandle = undefined
480+
481+
// Sanity check to verify the latest write is accessible immediately
482+
const heartbeatData = JSON.parse(await nodeFs.readFile(filePath, 'utf-8')) as ExtInstanceHeartbeat
477483
if (heartbeatData.lastHeartbeat !== now) {
478484
throw new CrashMonitoringError('Heartbeat write validation failed', { code: className })
479485
}
486+
487+
this.deps.devLogger?.debug(`crashMonitoring: HEARTBEAT sent for ${truncateUuid(this.ext.sessionId)}`)
480488
}
481489
const funcWithCtx = () => withFailCtx('sendHeartbeatState', func)
482490
const funcWithRetries = withRetries(funcWithCtx, { maxRetries: 6, delay: 100, backoff: 2 })
483-
const funcWithTelemetryRun = await telemetry.ide_heartbeat.run((span) => {
484-
span.record({ id: className, timestamp: now })
485-
return funcWithRetries
486-
})
487491

488-
return funcWithTelemetryRun
492+
return funcWithRetries
489493
} catch (e) {
494+
await fileHandle?.close()
495+
490496
// delete this ext from the state to avoid an incorrectly reported crash since we could not send a new heartbeat
491497
await this.deleteHeartbeatFile(extId, 'sendHeartbeatFailureCleanup')
492498
throw e
@@ -518,7 +524,21 @@ export class FileSystemState {
518524

519525
private async deleteHeartbeatFile(ext: ExtInstanceId | ExtInstance, ctx: string) {
520526
// IMPORTANT: Must use NodeFs here since this is used during shutdown
521-
const func = () => nodeFs.rm(this.makeStateFilePath(ext), { force: true })
527+
const func = async () => {
528+
const filePath = this.makeStateFilePath(ext)
529+
530+
// Even when deleting a file, if there is an open file handle it may still exist. This empties the
531+
// contents, so that any following reads will get no data.
532+
let fileHandle: nodeFs.FileHandle | undefined
533+
try {
534+
fileHandle = await nodeFs.open(filePath, 'w')
535+
await fileHandle.sync()
536+
} finally {
537+
await fileHandle?.close()
538+
}
539+
540+
await nodeFs.rm(filePath, { force: true })
541+
}
522542
const funcWithCtx = () => withFailCtx(ctx, func)
523543
const funcWithRetries = withRetries(funcWithCtx, { maxRetries: 6, delay: 100, backoff: 2 })
524544
await funcWithRetries
@@ -553,7 +573,7 @@ export class FileSystemState {
553573
public async clearState(): Promise<void> {
554574
this.deps.devLogger?.debug('crashMonitoring: CLEAR_STATE: Started')
555575
await withFailCtx('clearState', async () => {
556-
await fs.delete(this.stateDirPath, { force: true, recursive: true })
576+
await nodeFs.rm(this.stateDirPath, { force: true, recursive: true })
557577
this.deps.devLogger?.debug('crashMonitoring: CLEAR_STATE: Succeeded')
558578
})
559579
}
@@ -576,7 +596,7 @@ export class FileSystemState {
576596
// we will assume that other instance handled its termination appropriately.
577597
// NOTE: On Windows we were failing on EBUSY, so we retry on failure.
578598
const loadExtFromDisk = async () => {
579-
const text = await fs.readFileText(this.makeStateFilePath(extId))
599+
const text = await nodeFs.readFile(this.makeStateFilePath(extId), 'utf-8')
580600

581601
if (!text) {
582602
return undefined

packages/core/src/shared/env/resolveEnv.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { asEnvironmentVariables } from '../../auth/credentials/utils'
2020
import { getIAMConnection } from '../../auth/utils'
2121
import { ChildProcess } from '../utilities/processUtils'
2222

23-
let unixShellEnvPromise: Promise<typeof process.env> | undefined = undefined
23+
let unixShellEnvPromise: Promise<typeof process.env | void> | undefined = undefined
2424
let envCacheExpireTime: number
2525

2626
export interface IProcessEnvironment {
@@ -139,7 +139,7 @@ export async function mergeResolvedShellPath(env: IProcessEnvironment): Promise<
139139
* - we hit a timeout of `MAX_SHELL_RESOLVE_TIME`
140140
* - any other error from spawning a shell to figure out the environment
141141
*/
142-
export async function getResolvedShellEnv(env?: IProcessEnvironment): Promise<typeof process.env | undefined> {
142+
export async function getResolvedShellEnv(env?: IProcessEnvironment): Promise<typeof process.env | void> {
143143
if (!env) {
144144
env = process.env
145145
}
@@ -176,7 +176,7 @@ export async function getResolvedShellEnv(env?: IProcessEnvironment): Promise<ty
176176
if (!unixShellEnvPromise || Date.now() > envCacheExpireTime) {
177177
// cache valid for 5 minutes
178178
envCacheExpireTime = Date.now() + 5 * 60 * 1000
179-
unixShellEnvPromise = new Promise<NodeJS.ProcessEnv>(async (resolve, reject) => {
179+
unixShellEnvPromise = new Promise<NodeJS.ProcessEnv | void>(async (resolve, reject) => {
180180
const timeout = new Timeout(10000)
181181

182182
// Resolve shell env and handle errors
@@ -185,11 +185,11 @@ export async function getResolvedShellEnv(env?: IProcessEnvironment): Promise<ty
185185
if (shellEnv && Object.keys(shellEnv).length > 0) {
186186
resolve(shellEnv)
187187
} else {
188-
return undefined
188+
resolve()
189189
}
190190
} catch {
191191
// failed resolve should not affect other feature.
192-
return undefined
192+
resolve()
193193
}
194194
})
195195
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import nodeFs from 'fs/promises'
6+
import { TestFolder } from '../../../testUtil'
7+
import assert from 'assert'
8+
import { fs } from '../../../../shared'
9+
10+
describe('Node FS', () => {
11+
let testFolder: TestFolder
12+
13+
beforeEach(async function () {
14+
testFolder = await TestFolder.create()
15+
})
16+
17+
describe('open()', () => {
18+
it('"w" flag clears file content', async () => {
19+
const filePath = testFolder.pathFrom('file.txt')
20+
21+
// Make initial file with text
22+
await nodeFs.writeFile(filePath, 'test')
23+
assert.strictEqual(await fs.readFileText(filePath), 'test')
24+
25+
// Open file with "w"
26+
const fileHandle = await nodeFs.open(filePath, 'w')
27+
await fileHandle.close()
28+
29+
// file content was cleared
30+
assert.strictEqual(await fs.readFileText(filePath), '')
31+
})
32+
})
33+
34+
describe('sync()', () => {
35+
// we cannot accurately test if sync() works, so just assert nothing breaks when using it
36+
it('runs without error', async () => {
37+
const filePath = testFolder.pathFrom('file.txt')
38+
39+
// Make initial file with text
40+
await nodeFs.writeFile(filePath, 'test')
41+
assert.strictEqual(await fs.readFileText(filePath), 'test')
42+
43+
const fileHandle = await nodeFs.open(filePath, 'w')
44+
await fileHandle.writeFile('updatedText')
45+
await fileHandle.sync() // method under test
46+
await fileHandle.close()
47+
48+
// file content was cleared
49+
assert.strictEqual(await fs.readFileText(filePath), 'updatedText')
50+
})
51+
})
52+
})

0 commit comments

Comments
 (0)