Skip to content

Commit bf08577

Browse files
authored
Refactor Azure job submission (#2687)
Refactoring Azure job submission so that this PR is easier to read: #2686 There's a slight change in order of the telemetry events. We were sending `submitJob` twice when the Copilot tools were invoked. This is the only behavior change I'm expecting. Co-authored-by: Mine Starks <>
1 parent 9fed677 commit bf08577

File tree

6 files changed

+318
-315
lines changed

6 files changed

+318
-315
lines changed

source/vscode/src/azure/commands.ts

Lines changed: 144 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
import { log, TargetProfile } from "qsharp-lang";
45
import * as vscode from "vscode";
5-
import { log } from "qsharp-lang";
6-
6+
import { qsharpExtensionId } from "../common";
7+
import { FullProgramConfig, getActiveProgram } from "../programConfig";
8+
import { getQirForProgram, QirGenerationError } from "../qirGeneration";
9+
import {
10+
EventType,
11+
getActiveDocumentType,
12+
QsharpDocumentType,
13+
sendTelemetryEvent,
14+
UserFlowStatus,
15+
UserTaskInvocationType,
16+
} from "../telemetry";
17+
import { getRandomGuid } from "../utils";
18+
import { sendMessageToPanel } from "../webviewPanel";
19+
import { getTokenForWorkspace } from "./auth";
20+
import { QuantumUris } from "./networkRequests";
21+
import {
22+
getPreferredTargetProfile,
23+
targetSupportQir,
24+
} from "./providerProperties";
25+
import { startRefreshCycle } from "./treeRefresher";
726
import {
827
Job,
928
Target,
@@ -18,16 +37,6 @@ import {
1837
queryWorkspaces,
1938
submitJob,
2039
} from "./workspaceActions";
21-
import { QuantumUris } from "./networkRequests";
22-
import { getQirForActiveWindow } from "../qirGeneration";
23-
import {
24-
getPreferredTargetProfile,
25-
targetSupportQir,
26-
} from "./providerProperties";
27-
import { startRefreshCycle } from "./treeRefresher";
28-
import { getTokenForWorkspace } from "./auth";
29-
import { qsharpExtensionId } from "../common";
30-
import { sendMessageToPanel } from "../webviewPanel";
3140

3241
const workspacesSecret = `${qsharpExtensionId}.workspaces`;
3342

@@ -112,42 +121,33 @@ export async function initAzureWorkspaces(context: vscode.ExtensionContext) {
112121

113122
const target = treeItem.itemData as Target;
114123

115-
let qir = "";
116124
try {
117-
qir = await getQirForActiveWindow(
118-
getPreferredTargetProfile(target.id),
119-
);
120-
} catch (e: any) {
121-
if (e?.name === "QirGenerationError") {
122-
vscode.window.showErrorMessage(e.message, { modal: true });
123-
return;
124-
}
125-
}
126-
if (!qir) return;
125+
const preferredTargetProfile = getPreferredTargetProfile(target.id);
126+
const program = await getActiveProgram({
127+
showModalError: true,
128+
targetProfileFallback: preferredTargetProfile,
129+
});
127130

128-
const quantumUris = new QuantumUris(
129-
treeItem.workspace.endpointUri,
130-
treeItem.workspace.id,
131-
);
131+
if (!program.success) {
132+
throw new QirGenerationError(program.errorMsg);
133+
}
132134

133-
try {
134-
const token = await getTokenForWorkspace(treeItem.workspace);
135-
if (!token) return;
136-
137-
const jobId = await submitJob(
138-
token,
139-
quantumUris,
140-
qir,
141-
target.providerId,
142-
target.id,
135+
await compileAndSubmit(
136+
program.programConfig,
137+
preferredTargetProfile,
138+
getActiveDocumentType(),
139+
UserTaskInvocationType.Command,
140+
workspaceTreeProvider,
141+
treeItem.workspace,
142+
target,
143143
);
144-
if (jobId) {
145-
// The job submitted fine. Refresh the workspace until it shows up
146-
// and all jobs are finished. Don't await on this, just let it run
147-
startRefreshCycle(workspaceTreeProvider, treeItem.workspace, jobId);
144+
} catch (e: unknown) {
145+
log.warn("Failed to submit job. ", e);
146+
147+
if (e instanceof QirGenerationError) {
148+
vscode.window.showErrorMessage(e.message, { modal: true });
149+
return;
148150
}
149-
} catch (e: any) {
150-
log.error("Failed to submit job. ", e);
151151

152152
vscode.window.showErrorMessage("Failed to submit the job to Azure.", {
153153
modal: true,
@@ -401,3 +401,104 @@ function getHistogramBucketsFromData(
401401
}
402402
return undefined;
403403
}
404+
405+
export async function compileAndSubmit(
406+
program: FullProgramConfig,
407+
targetProfile: TargetProfile,
408+
telemetryDocumentType: QsharpDocumentType,
409+
telemetryInvocationType: UserTaskInvocationType,
410+
workspaceTreeProvider: WorkspaceTreeProvider,
411+
workspace: WorkspaceConnection,
412+
target: Target,
413+
parameters: { jobName: string; shots: number } | undefined = undefined,
414+
) {
415+
const associationId = getRandomGuid();
416+
const start = performance.now();
417+
sendTelemetryEvent(
418+
EventType.SubmitToAzureStart,
419+
{ associationId, invocationType: telemetryInvocationType },
420+
{},
421+
);
422+
423+
const qir = await getQirForProgram(
424+
program,
425+
targetProfile,
426+
telemetryDocumentType,
427+
);
428+
429+
if (!parameters) {
430+
const result = await promptForJobParameters();
431+
if (!result) {
432+
sendTelemetryEvent(
433+
EventType.SubmitToAzureEnd,
434+
{
435+
associationId,
436+
reason: "user cancelled parameter input",
437+
flowStatus: UserFlowStatus.Aborted,
438+
},
439+
{ timeToCompleteMs: performance.now() - start },
440+
);
441+
return;
442+
}
443+
parameters = { jobName: result.jobName, shots: result.numberOfShots };
444+
}
445+
446+
const { jobId } = await submitJob(
447+
workspace,
448+
associationId,
449+
qir,
450+
target.providerId,
451+
target.id,
452+
parameters.jobName,
453+
parameters.shots,
454+
);
455+
456+
sendTelemetryEvent(
457+
EventType.SubmitToAzureEnd,
458+
{
459+
associationId,
460+
reason: "job submitted",
461+
flowStatus: UserFlowStatus.Succeeded,
462+
},
463+
{ timeToCompleteMs: performance.now() - start },
464+
);
465+
466+
// The job submitted fine. Refresh the workspace until it shows up
467+
// and all jobs are finished. Don't await on this, just let it run
468+
startRefreshCycle(workspaceTreeProvider, workspace, jobId);
469+
470+
return jobId;
471+
}
472+
473+
async function promptForJobParameters(): Promise<
474+
{ jobName: string; numberOfShots: number } | undefined
475+
> {
476+
const jobName = await vscode.window.showInputBox({
477+
prompt: "Job name",
478+
value: new Date().toISOString(),
479+
});
480+
if (!jobName) return;
481+
482+
// validator for the user-provided number of shots input
483+
const validateShotsInput = (input: string) => {
484+
const result = parseFloat(input);
485+
if (isNaN(result) || Math.floor(result) !== result) {
486+
return "Number of shots must be an integer";
487+
}
488+
};
489+
490+
// prompt the user for the number of shots
491+
const numberOfShotsPrompted = await vscode.window.showInputBox({
492+
value: "100",
493+
prompt: "Number of shots",
494+
validateInput: validateShotsInput,
495+
});
496+
497+
// abort if the user hits <Esc> during shots entry
498+
if (numberOfShotsPrompted === undefined) {
499+
return;
500+
}
501+
502+
const numberOfShots = parseInt(numberOfShotsPrompted);
503+
return { jobName, numberOfShots };
504+
}

source/vscode/src/azure/networkRequests.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { log } from "qsharp-lang";
55
import { EventType, getUserAgent, sendTelemetryEvent } from "../telemetry";
6+
import * as vscode from "vscode";
67

78
export async function azureRequest(
89
uri: string,
@@ -240,30 +241,35 @@ export class QuantumUris {
240241
}
241242

242243
export class StorageUris {
243-
// Requests use a Shared Access Signature. See https://learn.microsoft.com/en-us/rest/api/storageservices/service-sas-examples
244+
private storageAccount: string;
245+
private sasTokenRaw: string;
244246

245-
// x-ms-date header should be present in format: Sun, 06 Nov 1994 08:49:37 GMT
246-
// See https://learn.microsoft.com/en-us/rest/api/storageservices/representation-of-date-time-values-in-headers
247+
constructor(
248+
private sasUri: string,
249+
private containerName: string,
250+
) {
251+
// Parse the Uri to get the storage account and sasToken
252+
const sasUriObj = vscode.Uri.parse(sasUri);
253+
this.storageAccount = sasUriObj.scheme + "://" + sasUriObj.authority;
247254

248-
readonly apiVersion = "2023-01-03"; // Pass as x-ms-version header (see https://learn.microsoft.com/en-us/rest/api/storageservices/versioning-for-the-azure-storage-services#authorize-requests-by-using-azure-ad-shared-key-or-shared-key-lite)
255+
// Get the raw value to append to other query strings
256+
this.sasTokenRaw = sasUri.substring(sasUri.indexOf("?") + 1);
257+
}
249258

250-
// Same for PUT, with a status code of 201 if successful
251-
getContainer(storageAccount: string, container: string, sas: string) {
252-
return `https://${storageAccount}.blob.core.windows.net/${container}?restype=container&${sas}`;
259+
blob(blobName: string) {
260+
return `${this.storageAccount}/${this.containerName}/${blobName}`;
253261
}
254262

255-
// Same for DELETE, with a status code of 202 if successful
256-
// Also same URI for PUT, but must include the following headers:
257-
// - x-ms-blob-type: BlockBlob
258-
// - Content-Length: <n>
259-
// It will return 201 if created.
260-
getBlob(
261-
storageAccount: string,
262-
container: string,
263-
blob: string,
264-
sas: string,
265-
) {
266-
return `https://${storageAccount}.blob.core.windows.net/${container}/${blob}?${sas}`;
263+
blobWithSasToken(blobName: string) {
264+
return `${this.storageAccount}/${this.containerName}/${blobName}?${this.sasTokenRaw}`;
265+
}
266+
267+
containerWithSasToken() {
268+
return this.sasUri;
269+
}
270+
271+
containerPutWithSasToken() {
272+
return `${this.storageAccount}/${this.containerName}?restype=container&${this.sasTokenRaw}`;
267273
}
268274
}
269275

0 commit comments

Comments
 (0)