mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-23 06:45:27 +02:00
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:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user