Feat/concurrent deployments in memory queue (#4645)

* feat: add builds concurrency management for servers

- Introduced a new `BuildsConcurrency` component to manage the number of concurrent builds for both local and remote servers, gated by license validity.
- Implemented backend logic to resolve effective builds concurrency based on server settings and organization licenses.
- Added unit tests for concurrency resolution logic to ensure correct behavior under various licensing scenarios.
- Updated database schema to include `buildsConcurrency` field for servers and web server settings.
- Refactored deployment queue to support in-memory job processing with configurable concurrency limits.

This feature enhances deployment flexibility and control for enterprise users.

* refactor: enhance deployment cancellation logic and improve Railpack build isolation

- Reintroduced the `initCancelDeployments` function in the server initialization sequence to ensure deployments can be canceled effectively.
- Updated the Railpack build command to use a unique builder name for each build, preventing conflicts during concurrent deployments.
- Enhanced the cancellation logic to reset application and compose statuses to "idle" after canceling running deployments, improving system reliability.

* test: add buildsConcurrency setting to server configuration tests

- Introduced a new `buildsConcurrency` property in the server configuration tests to ensure proper handling of concurrent builds in deployment scenarios.

* feat: implement builds concurrency management and validation

- Added `assertBuildsConcurrencyAllowed` function to validate concurrency settings based on license status.
- Updated `resolveBuildsConcurrency` to reflect new concurrency limits for free and enterprise tiers.
- Enhanced `BuildsConcurrency` component to manage concurrent builds for servers, with UI adjustments for better user experience.
- Introduced a new settings page for managing concurrent builds across servers, ensuring proper handling of deployments.
- Updated database schema to support increased maximum concurrency values for servers and web server settings.
This commit is contained in:
Mauricio Siu
2026-06-16 23:15:19 -06:00
committed by GitHub
parent 439f575669
commit 24b02f5523
23 changed files with 9842 additions and 157 deletions

View File

@@ -0,0 +1,122 @@
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
// Free tier may set up to 2 concurrent builds; enterprise unlocks more.
const FREE_MAX_CONCURRENCY = 2;
const ENTERPRISE_MAX_CONCURRENCY = 100;
interface Props {
/**
* When provided, configures concurrency for that remote server. When
* omitted, configures the local Dokploy web server.
*/
serverId?: string;
/** Optional title override (e.g. the server name in a list). */
label?: string;
}
/**
* Control to set the number of concurrent builds, either for a remote server
* (`serverId` provided) or the local web server (omitted). Available to
* everyone self-hosted up to FREE_MAX_CONCURRENCY; higher values require a
* valid enterprise license. Not shown in cloud.
*/
export const BuildsConcurrency = ({ serverId, label }: Props) => {
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: haveValidLicense } =
api.licenseKey.haveValidLicenseKey.useQuery();
const serverQuery = api.server.one.useQuery(
{ serverId: serverId ?? "" },
{ enabled: !!serverId },
);
const webServerQuery = api.settings.getWebServerSettings.useQuery(undefined, {
enabled: !serverId,
});
const current = serverId
? serverQuery.data?.buildsConcurrency
: webServerQuery.data?.buildsConcurrency;
const refetch = serverId ? serverQuery.refetch : webServerQuery.refetch;
const updateServer = api.server.updateBuildsConcurrency.useMutation();
const updateWebServer = api.settings.updateBuildsConcurrency.useMutation();
const isPending = serverId
? updateServer.isPending
: updateWebServer.isPending;
const [value, setValue] = useState("1");
useEffect(() => {
if (current) {
setValue(String(current));
}
}, [current]);
// Concurrent builds are a self-hosted feature; not shown in cloud.
if (isCloud) return null;
const max = haveValidLicense
? ENTERPRISE_MAX_CONCURRENCY
: FREE_MAX_CONCURRENCY;
const clamp = (n: number) => Math.min(max, Math.max(1, n));
const handleSave = async () => {
const parsed = clamp(Number.parseInt(value, 10) || 1);
setValue(String(parsed));
try {
if (serverId) {
await updateServer.mutateAsync({ serverId, buildsConcurrency: parsed });
} else {
await updateWebServer.mutateAsync({ buildsConcurrency: parsed });
}
await refetch();
toast.success("Builds concurrency updated");
} catch {
toast.error("Error updating builds concurrency");
}
};
const hasChanges = Number(value) !== (current ?? 1);
return (
<div className="flex flex-col gap-3 rounded-lg border p-3">
<div className="flex flex-row items-center justify-between gap-4">
<div className="space-y-0.5">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">
{label ?? serverQuery.data?.name ?? "Dokploy Server"}
</p>
<span className="text-xs text-muted-foreground rounded border px-1.5 py-0.5">
{serverId
? (serverQuery.data?.ipAddress ?? "remote server")
: "local host"}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<Input
type="number"
min={1}
max={max}
value={value}
onChange={(e) => setValue(e.target.value)}
className="w-20"
/>
<Button
type="button"
size="sm"
onClick={handleSave}
isLoading={isPending}
disabled={!hasChanges}
>
Save
</Button>
</div>
</div>
</div>
);
};

View File

@@ -298,6 +298,14 @@ const MENU: Menu = {
icon: Server,
isEnabled: ({ permissions }) => !!permissions?.server.read,
},
{
isSingle: true,
title: "Deployments",
url: "/dashboard/settings/deployments",
icon: Boxes,
isEnabled: ({ permissions, isCloud }) =>
!!(permissions?.server.read && !isCloud),
},
{
isSingle: true,
title: "Users",