feat: implement tweakable utility workers concurrency, instead of hard-coded value set to 5
This commit is contained in:
7
app.py
7
app.py
@@ -51,7 +51,6 @@ if _umask_value:
|
|||||||
# Defer logging setup; avoid failing on invalid UMASK
|
# Defer logging setup; avoid failing on invalid UMASK
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Import and initialize routes (this will start the watch manager)
|
# Import and initialize routes (this will start the watch manager)
|
||||||
from routes.auth.credentials import router as credentials_router # noqa: E402
|
from routes.auth.credentials import router as credentials_router # noqa: E402
|
||||||
from routes.auth.auth import router as auth_router # noqa: E402
|
from routes.auth.auth import router as auth_router # noqa: E402
|
||||||
@@ -65,14 +64,8 @@ from routes.core.history import router as history_router # noqa: E402
|
|||||||
from routes.system.progress import router as prgs_router # noqa: E402
|
from routes.system.progress import router as prgs_router # noqa: E402
|
||||||
from routes.system.config import router as config_router # noqa: E402
|
from routes.system.config import router as config_router # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
# Import Celery configuration and manager
|
|
||||||
from routes.utils.celery_config import REDIS_URL # noqa: E402
|
from routes.utils.celery_config import REDIS_URL # noqa: E402
|
||||||
|
|
||||||
# Import authentication system
|
|
||||||
|
|
||||||
# Import watch manager controls (start/stop) without triggering side effects
|
|
||||||
|
|
||||||
|
|
||||||
# Configure application-wide logging
|
# Configure application-wide logging
|
||||||
def setup_logging():
|
def setup_logging():
|
||||||
|
|||||||
@@ -110,6 +110,76 @@ function SpotifyApiForm() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function UtilityConcurrencyForm() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { data: configData, isLoading } = useQuery({
|
||||||
|
queryKey: ["config"],
|
||||||
|
queryFn: () => authApiClient.getConfig<any>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { register, handleSubmit, reset, formState: { isDirty } } = useForm<{ utilityConcurrency: number }>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (configData) {
|
||||||
|
reset({ utilityConcurrency: Number(configData.utilityConcurrency ?? 1) });
|
||||||
|
}
|
||||||
|
}, [configData, reset]);
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (payload: { utilityConcurrency: number }) => authApiClient.updateConfig(payload),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Utility worker concurrency saved!");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["config"] });
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast.error(`Failed to save: ${(e as any).message}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (values: { utilityConcurrency: number }) => {
|
||||||
|
const value = Math.max(1, Number(values.utilityConcurrency || 1));
|
||||||
|
mutation.mutate({ utilityConcurrency: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <p className="text-content-muted dark:text-content-muted-dark">Loading server settings...</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<div className="flex items-center justify-end mb-2">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={mutation.isPending || !isDirty}
|
||||||
|
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||||
|
title="Save Utility Concurrency"
|
||||||
|
>
|
||||||
|
{mutation.isPending ? (
|
||||||
|
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
||||||
|
) : (
|
||||||
|
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label htmlFor="utilityConcurrency" className="text-content-primary dark:text-content-primary-dark">Utility Worker Concurrency</label>
|
||||||
|
<input
|
||||||
|
id="utilityConcurrency"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
{...register("utilityConcurrency", { valueAsNumber: true })}
|
||||||
|
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
|
||||||
|
placeholder="1"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-content-secondary dark:text-content-secondary-dark">Controls concurrency of the utility Celery worker. Minimum 1.</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Components ---
|
||||||
function WebhookForm() {
|
function WebhookForm() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data, isLoading } = useQuery({ queryKey: ["webhookConfig"], queryFn: fetchWebhookConfig });
|
const { data, isLoading } = useQuery({ queryKey: ["webhookConfig"], queryFn: fetchWebhookConfig });
|
||||||
@@ -223,6 +293,12 @@ export function ServerTab() {
|
|||||||
<SpotifyApiForm />
|
<SpotifyApiForm />
|
||||||
</div>
|
</div>
|
||||||
<hr className="border-border dark:border-border-dark" />
|
<hr className="border-border dark:border-border-dark" />
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Utility Worker</h3>
|
||||||
|
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">Tune background utility worker concurrency for low-powered systems.</p>
|
||||||
|
<UtilityConcurrencyForm />
|
||||||
|
</div>
|
||||||
|
<hr className="border-border dark:border-border-dark" />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Webhooks</h3>
|
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Webhooks</h3>
|
||||||
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">
|
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export type FlatAppSettings = {
|
|||||||
deezer: string;
|
deezer: string;
|
||||||
deezerQuality: "MP3_128" | "MP3_320" | "FLAC";
|
deezerQuality: "MP3_128" | "MP3_320" | "FLAC";
|
||||||
maxConcurrentDownloads: number;
|
maxConcurrentDownloads: number;
|
||||||
|
utilityConcurrency: number;
|
||||||
realTime: boolean;
|
realTime: boolean;
|
||||||
fallback: boolean;
|
fallback: boolean;
|
||||||
convertTo: "MP3" | "AAC" | "OGG" | "OPUS" | "FLAC" | "WAV" | "ALAC" | "";
|
convertTo: "MP3" | "AAC" | "OGG" | "OPUS" | "FLAC" | "WAV" | "ALAC" | "";
|
||||||
@@ -72,6 +73,7 @@ const defaultSettings: FlatAppSettings = {
|
|||||||
deezer: "",
|
deezer: "",
|
||||||
deezerQuality: "MP3_128",
|
deezerQuality: "MP3_128",
|
||||||
maxConcurrentDownloads: 3,
|
maxConcurrentDownloads: 3,
|
||||||
|
utilityConcurrency: 1,
|
||||||
realTime: false,
|
realTime: false,
|
||||||
fallback: false,
|
fallback: false,
|
||||||
convertTo: "",
|
convertTo: "",
|
||||||
@@ -135,6 +137,7 @@ const fetchSettings = async (): Promise<FlatAppSettings> => {
|
|||||||
// Ensure required frontend-only fields exist
|
// Ensure required frontend-only fields exist
|
||||||
recursiveQuality: Boolean((camelData as any).recursiveQuality ?? false),
|
recursiveQuality: Boolean((camelData as any).recursiveQuality ?? false),
|
||||||
realTimeMultiplier: Number((camelData as any).realTimeMultiplier ?? 0),
|
realTimeMultiplier: Number((camelData as any).realTimeMultiplier ?? 0),
|
||||||
|
utilityConcurrency: Number((camelData as any).utilityConcurrency ?? 1),
|
||||||
// Ensure watch subkeys default if missing
|
// Ensure watch subkeys default if missing
|
||||||
watch: {
|
watch: {
|
||||||
...(camelData.watch as any),
|
...(camelData.watch as any),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface AppSettings {
|
|||||||
deezer: string;
|
deezer: string;
|
||||||
deezerQuality: "MP3_128" | "MP3_320" | "FLAC";
|
deezerQuality: "MP3_128" | "MP3_320" | "FLAC";
|
||||||
maxConcurrentDownloads: number;
|
maxConcurrentDownloads: number;
|
||||||
|
utilityConcurrency: number;
|
||||||
realTime: boolean;
|
realTime: boolean;
|
||||||
fallback: boolean;
|
fallback: boolean;
|
||||||
convertTo: "MP3" | "AAC" | "OGG" | "OPUS" | "FLAC" | "WAV" | "ALAC" | "";
|
convertTo: "MP3" | "AAC" | "OGG" | "OPUS" | "FLAC" | "WAV" | "ALAC" | "";
|
||||||
|
|||||||
@@ -369,6 +369,17 @@ class AuthApiClient {
|
|||||||
get client() {
|
get client() {
|
||||||
return this.apiClient;
|
return this.apiClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// General config helpers
|
||||||
|
async getConfig<T = any>(): Promise<T> {
|
||||||
|
const response = await this.apiClient.get<T>("/config");
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateConfig<T = any>(partial: Record<string, unknown>): Promise<T> {
|
||||||
|
const response = await this.apiClient.put<T>("/config", partial);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and export a singleton instance
|
// Create and export a singleton instance
|
||||||
|
|||||||
Reference in New Issue
Block a user