Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/parallel-deploy-rpcs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tailor-platform/sdk": patch
---

Speed up deploy by running SDK version detection and function uploads concurrently
42 changes: 21 additions & 21 deletions packages/sdk/docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,27 +65,27 @@ tailor-sdk deploy --env-file .env --env-file .env.production

You can use environment variables to configure workspace and authentication:

| Variable | Description |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `TAILOR_PLATFORM_WORKSPACE_ID` | Workspace ID for deployment commands |
| `TAILOR_PLATFORM_ORGANIZATION_ID` | Organization ID for organization commands |
| `TAILOR_PLATFORM_FOLDER_ID` | Folder ID for folder commands |
| `TAILOR_PLATFORM_TOKEN` | Authentication token (alternative to `login`) |
| `TAILOR_TOKEN` | **Deprecated.** Use `TAILOR_PLATFORM_TOKEN` instead |
| `TAILOR_PLATFORM_PROFILE` | Workspace profile name |
| `TAILOR_PLATFORM_SDK_CONFIG_PATH` | Path to SDK config file |
| `TAILOR_PLATFORM_SDK_DTS_PATH` | Output path for generated `tailor.d.ts` type definition file |
| `TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID` | Client ID for `login --machine-user` |
| `TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET` | Client secret for `login --machine-user` |
| `TAILOR_PLATFORM_MACHINE_USER_NAME` | Default machine user name for `query`, `workflow start`, `function test-run`, `machineuser token` |
| `TAILOR_PLATFORM_URL` | Platform API base URL. Saved into profiles created with `profile create --platform-url` |
| `TAILOR_PLATFORM_OAUTH2_CLIENT_ID` | OAuth2 client ID for user login. Saved into profiles created with `profile create --oauth2-client-id` |
| `TAILOR_PLATFORM_CONSOLE_URL` | Console base URL. Saved into profiles created with `profile create --console-url` |
| `TAILOR_BUNDLE_CONCURRENCY` | Max concurrent bundle workers for `deploy` (resolvers/executors/workflows). Defaults to CPU count |
| `TAILOR_APPLY_CONCURRENCY` | Max concurrent unary platform RPCs during `apply`/`deploy` (streaming uploads are not gated). Defaults to 16 |
| `VISUAL` / `EDITOR` | Preferred editor for commands that open files (e.g., `vim`, `code`, `nano`) |
| `TAILOR_CRASH_REPORTS_LOCAL` | Local crash log writing: `on` (default) or `off` |
| `TAILOR_CRASH_REPORTS_REMOTE` | Automatic crash report submission: `off` (default) or `on` |
| Variable | Description |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `TAILOR_PLATFORM_WORKSPACE_ID` | Workspace ID for deployment commands |
| `TAILOR_PLATFORM_ORGANIZATION_ID` | Organization ID for organization commands |
| `TAILOR_PLATFORM_FOLDER_ID` | Folder ID for folder commands |
| `TAILOR_PLATFORM_TOKEN` | Authentication token (alternative to `login`) |
| `TAILOR_TOKEN` | **Deprecated.** Use `TAILOR_PLATFORM_TOKEN` instead |
| `TAILOR_PLATFORM_PROFILE` | Workspace profile name |
| `TAILOR_PLATFORM_SDK_CONFIG_PATH` | Path to SDK config file |
| `TAILOR_PLATFORM_SDK_DTS_PATH` | Output path for generated `tailor.d.ts` type definition file |
| `TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID` | Client ID for `login --machine-user` |
| `TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET` | Client secret for `login --machine-user` |
| `TAILOR_PLATFORM_MACHINE_USER_NAME` | Default machine user name for `query`, `workflow start`, `function test-run`, `machineuser token` |
| `TAILOR_PLATFORM_URL` | Platform API base URL. Saved into profiles created with `profile create --platform-url` |
| `TAILOR_PLATFORM_OAUTH2_CLIENT_ID` | OAuth2 client ID for user login. Saved into profiles created with `profile create --oauth2-client-id` |
| `TAILOR_PLATFORM_CONSOLE_URL` | Console base URL. Saved into profiles created with `profile create --console-url` |
| `TAILOR_BUNDLE_CONCURRENCY` | Max concurrent bundle workers for `deploy` (resolvers/executors/workflows). Defaults to CPU count |
| `TAILOR_APPLY_CONCURRENCY` | Max concurrent platform RPCs during `apply`/`deploy`. Defaults to 16 |
| `VISUAL` / `EDITOR` | Preferred editor for commands that open files (e.g., `vim`, `code`, `nano`) |
| `TAILOR_CRASH_REPORTS_LOCAL` | Local crash log writing: `on` (default) or `off` |
| `TAILOR_CRASH_REPORTS_REMOTE` | Automatic crash report submission: `off` (default) or `on` |

### Authentication Token Priority

Expand Down
42 changes: 21 additions & 21 deletions packages/sdk/docs/cli-reference.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,27 +58,27 @@ tailor-sdk deploy --env-file .env --env-file .env.production

You can use environment variables to configure workspace and authentication:

| Variable | Description |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `TAILOR_PLATFORM_WORKSPACE_ID` | Workspace ID for deployment commands |
| `TAILOR_PLATFORM_ORGANIZATION_ID` | Organization ID for organization commands |
| `TAILOR_PLATFORM_FOLDER_ID` | Folder ID for folder commands |
| `TAILOR_PLATFORM_TOKEN` | Authentication token (alternative to `login`) |
| `TAILOR_TOKEN` | **Deprecated.** Use `TAILOR_PLATFORM_TOKEN` instead |
| `TAILOR_PLATFORM_PROFILE` | Workspace profile name |
| `TAILOR_PLATFORM_SDK_CONFIG_PATH` | Path to SDK config file |
| `TAILOR_PLATFORM_SDK_DTS_PATH` | Output path for generated `tailor.d.ts` type definition file |
| `TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID` | Client ID for `login --machine-user` |
| `TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET` | Client secret for `login --machine-user` |
| `TAILOR_PLATFORM_MACHINE_USER_NAME` | Default machine user name for `query`, `workflow start`, `function test-run`, `machineuser token` |
| `TAILOR_PLATFORM_URL` | Platform API base URL. Saved into profiles created with `profile create --platform-url` |
| `TAILOR_PLATFORM_OAUTH2_CLIENT_ID` | OAuth2 client ID for user login. Saved into profiles created with `profile create --oauth2-client-id` |
| `TAILOR_PLATFORM_CONSOLE_URL` | Console base URL. Saved into profiles created with `profile create --console-url` |
| `TAILOR_BUNDLE_CONCURRENCY` | Max concurrent bundle workers for `deploy` (resolvers/executors/workflows). Defaults to CPU count |
| `TAILOR_APPLY_CONCURRENCY` | Max concurrent unary platform RPCs during `apply`/`deploy` (streaming uploads are not gated). Defaults to 16 |
| `VISUAL` / `EDITOR` | Preferred editor for commands that open files (e.g., `vim`, `code`, `nano`) |
| `TAILOR_CRASH_REPORTS_LOCAL` | Local crash log writing: `on` (default) or `off` |
| `TAILOR_CRASH_REPORTS_REMOTE` | Automatic crash report submission: `off` (default) or `on` |
| Variable | Description |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `TAILOR_PLATFORM_WORKSPACE_ID` | Workspace ID for deployment commands |
| `TAILOR_PLATFORM_ORGANIZATION_ID` | Organization ID for organization commands |
| `TAILOR_PLATFORM_FOLDER_ID` | Folder ID for folder commands |
| `TAILOR_PLATFORM_TOKEN` | Authentication token (alternative to `login`) |
| `TAILOR_TOKEN` | **Deprecated.** Use `TAILOR_PLATFORM_TOKEN` instead |
| `TAILOR_PLATFORM_PROFILE` | Workspace profile name |
| `TAILOR_PLATFORM_SDK_CONFIG_PATH` | Path to SDK config file |
| `TAILOR_PLATFORM_SDK_DTS_PATH` | Output path for generated `tailor.d.ts` type definition file |
| `TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID` | Client ID for `login --machine-user` |
| `TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET` | Client secret for `login --machine-user` |
| `TAILOR_PLATFORM_MACHINE_USER_NAME` | Default machine user name for `query`, `workflow start`, `function test-run`, `machineuser token` |
| `TAILOR_PLATFORM_URL` | Platform API base URL. Saved into profiles created with `profile create --platform-url` |
| `TAILOR_PLATFORM_OAUTH2_CLIENT_ID` | OAuth2 client ID for user login. Saved into profiles created with `profile create --oauth2-client-id` |
| `TAILOR_PLATFORM_CONSOLE_URL` | Console base URL. Saved into profiles created with `profile create --console-url` |
| `TAILOR_BUNDLE_CONCURRENCY` | Max concurrent bundle workers for `deploy` (resolvers/executors/workflows). Defaults to CPU count |
| `TAILOR_APPLY_CONCURRENCY` | Max concurrent platform RPCs during `apply`/`deploy`. Defaults to 16 |
| `VISUAL` / `EDITOR` | Preferred editor for commands that open files (e.g., `vim`, `code`, `nano`) |
| `TAILOR_CRASH_REPORTS_LOCAL` | Local crash log writing: `on` (default) or `off` |
| `TAILOR_CRASH_REPORTS_REMOTE` | Automatic crash report submission: `off` (default) or `on` |

### Authentication Token Priority

Expand Down
84 changes: 84 additions & 0 deletions packages/sdk/src/cli/commands/deploy/deploy.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { logger } from "#/cli/shared/logger";
import { createConcurrencyProbe } from "#/cli/shared/test-helpers/concurrency-probe";
import { jsonMode } from "#/cli/shared/test-helpers/json-mode";
import { silenceLogger } from "#/cli/shared/test-helpers/silence-logger";
import { createChangeSet } from "./change-set";
Expand All @@ -18,6 +19,7 @@ import {
planDeploymentTargets,
printDeploymentPlans,
printPlanResults,
shouldForceApplyAll,
summarizePlanResults,
} from "./deploy";
import type { PlannedDeployment } from "./apply-phases";
Expand Down Expand Up @@ -144,6 +146,88 @@ function plannedDeployment(name: string, results: PlanResults): PlannedDeploymen
} as unknown as PlannedDeployment;
}

describe("shouldForceApplyAll", () => {
type Client = Parameters<typeof shouldForceApplyAll>[0];
type App = Parameters<typeof shouldForceApplyAll>[2];

function minimalApplication(): App {
return {
name: "test-app",
id: "test-app-id",
subgraphs: [],
staticWebsiteServices: [],
aiGatewayServices: [],
resolverServices: [],
idpServices: [],
authService: undefined,
executorService: undefined,
workflowService: undefined,
tailorDBServices: [],
secrets: [],
} as unknown as App;
}

test("fetches candidate metadata concurrently", async () => {
const probe = createConcurrencyProbe();
const getMetadata = vi.fn().mockImplementation(async () => {
await probe.run();
return { metadata: { labels: {} } };
});
const client = { getMetadata } as unknown as Client;

const result = await shouldForceApplyAll(client, "test-workspace", minimalApplication(), [
{ name: "fn-a" },
{ name: "fn-b" },
{ name: "fn-c" },
]);

expect(result).toBe(false);
expect(getMetadata).toHaveBeenCalledTimes(3);
expect(probe.maxInFlight()).toBeGreaterThan(1);
});

test("returns true when an owned resource has a different sdk-version", async () => {
const getMetadata = vi.fn().mockResolvedValue({
metadata: { labels: { "sdk-name": "test-app", "sdk-version": "v0-0-0-other" } },
});
const client = { getMetadata } as unknown as Client;

const result = await shouldForceApplyAll(client, "test-workspace", minimalApplication(), [
{ name: "fn-a" },
]);

expect(result).toBe(true);
});

test("prefers a detected sdk-version mismatch over an unrelated fetch failure", async () => {
const getMetadata = vi.fn().mockImplementation(({ trn }: { trn: string }) => {
if (trn.endsWith(":fn-a")) {
return Promise.resolve({
metadata: { labels: { "sdk-name": "test-app", "sdk-version": "v0-0-0-other" } },
});
}
return Promise.reject(new Error("unavailable"));
});
const client = { getMetadata } as unknown as Client;

const result = await shouldForceApplyAll(client, "test-workspace", minimalApplication(), [
{ name: "fn-a" },
{ name: "fn-b" },
]);

expect(result).toBe(true);
});

test("propagates a fetch failure when no mismatch was detected", async () => {
const getMetadata = vi.fn().mockRejectedValue(new Error("unavailable"));
const client = { getMetadata } as unknown as Client;

await expect(
shouldForceApplyAll(client, "test-workspace", minimalApplication(), [{ name: "fn-a" }]),
).rejects.toThrow("unavailable");
});
});

describe("summarizePlanResults", () => {
test("counts display entries and service actions", () => {
const displayEntries: GroupedDisplayEntry[] = [
Expand Down
44 changes: 31 additions & 13 deletions packages/sdk/src/cli/commands/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,16 @@ export function collectVisibleResolverNamespaces(
});
}

async function shouldForceApplyAll(
/**
* Detect whether any resource owned by this application was last applied by a
* different SDK version, in which case every resource is re-applied.
* @param client - Operator client instance
* @param workspaceId - Workspace ID
* @param application - Application being deployed
* @param functionEntries - Function registry entries of the application
* @returns True when an owned resource carries a different sdk-version label
*/
export async function shouldForceApplyAll(
client: OperatorClient,
workspaceId: string,
application: Readonly<Application>,
Expand Down Expand Up @@ -378,19 +387,28 @@ async function shouldForceApplyAll(
candidateTrns.add(resourceTrn(workspaceId, "function_registry", entry.name));
});

for (const trn of candidateTrns) {
const metadata = await getOrNull(async () => {
const { metadata } = await client.getMetadata({ trn });
return metadata;
});
if (metadata?.labels[sdkNameLabelKey] !== application.name) {
continue;
}
if (!hasMatchingSdkVersion(metadata.labels, desiredLabels)) {
return true;
}
}
const results = await Promise.allSettled(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concurrent getMetadata fetch has no concurrency cap.

function-registry.ts bounds its uploads with createApplyLimiter() (TAILOR_APPLY_CONCURRENCY), but this one fires getMetadata for every candidate TRN at once.

On projects with many resources this risks overloading the platform. Suggest limiting it with the same createApplyLimiter().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getMetadata is a unary RPC, so these calls already pass through the concurrencyLimitInterceptor() installed by initOperatorClient(). That interceptor shares a single createApplyLimiter() budget across all unary calls on the client, so adding another call-site limiter here would only duplicate the existing cap.

[...candidateTrns].map((trn) =>
getOrNull(async () => {
const { metadata } = await client.getMetadata({ trn });
return metadata;
}),
),
);

const hasMismatch = results.some(
(result) =>
result.status === "fulfilled" &&
result.value?.labels[sdkNameLabelKey] === application.name &&
!hasMatchingSdkVersion(result.value.labels, desiredLabels),
);
if (hasMismatch) {
return true;
}
const failure = results.find((result) => result.status === "rejected");
if (failure) {
throw failure.reason;
}
return false;
}

Expand Down
Loading
Loading