feat(worker): configurable max parallel task executions
Add a "Max parallel executions" setting to the General settings tab so the queue can run more than one task concurrently. QueueService now tracks multiple active slots and reads the limit from app settings each cycle, so changes take effect without restarting the worker. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,9 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
|
|||||||
builder.Property(s => s.DefaultPermissionMode)
|
builder.Property(s => s.DefaultPermissionMode)
|
||||||
.HasColumnName("default_permission_mode").IsRequired().HasDefaultValue("bypassPermissions");
|
.HasColumnName("default_permission_mode").IsRequired().HasDefaultValue("bypassPermissions");
|
||||||
|
|
||||||
|
builder.Property(s => s.MaxParallelExecutions)
|
||||||
|
.HasColumnName("max_parallel_executions").IsRequired().HasDefaultValue(1);
|
||||||
|
|
||||||
builder.Property(s => s.WorktreeStrategy)
|
builder.Property(s => s.WorktreeStrategy)
|
||||||
.HasColumnName("worktree_strategy").IsRequired().HasDefaultValue("sibling");
|
.HasColumnName("worktree_strategy").IsRequired().HasDefaultValue("sibling");
|
||||||
builder.Property(s => s.CentralWorktreeRoot)
|
builder.Property(s => s.CentralWorktreeRoot)
|
||||||
|
|||||||
607
src/ClaudeDo.Data/Migrations/20260601133737_AddMaxParallelExecutions.Designer.cs
generated
Normal file
607
src/ClaudeDo.Data/Migrations/20260601133737_AddMaxParallelExecutions.Designer.cs
generated
Normal file
@@ -0,0 +1,607 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ClaudeDoDbContext))]
|
||||||
|
[Migration("20260601133737_AddMaxParallelExecutions")]
|
||||||
|
partial class AddMaxParallelExecutions
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("CentralWorktreeRoot")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("central_worktree_root");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultClaudeInstructions")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("default_claude_instructions");
|
||||||
|
|
||||||
|
b.Property<int>("DefaultMaxTurns")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(30)
|
||||||
|
.HasColumnName("default_max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultModel")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sonnet")
|
||||||
|
.HasColumnName("default_model");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultPermissionMode")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("bypassPermissions")
|
||||||
|
.HasColumnName("default_permission_mode");
|
||||||
|
|
||||||
|
b.Property<int>("MaxParallelExecutions")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("max_parallel_executions");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<int>("WorktreeAutoCleanupDays")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(7)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_days");
|
||||||
|
|
||||||
|
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||||
|
|
||||||
|
b.Property<string>("WorktreeStrategy")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sibling")
|
||||||
|
.HasColumnName("worktree_strategy");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("app_settings", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.HasKey("ListId");
|
||||||
|
|
||||||
|
b.ToTable("list_config", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultCommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("default_commit_type");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("WorkingDir")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("working_dir");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SortOrder")
|
||||||
|
.HasDatabaseName("idx_lists_sort");
|
||||||
|
|
||||||
|
b.ToTable("lists", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.Property<bool>("WorkdaysOnly")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("workdays_only");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<bool>("Completed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("completed");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("OrderNum")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("order_num");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_subtasks_task_id");
|
||||||
|
|
||||||
|
b.ToTable("subtasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<string>("BlockedByTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("blocked_by_task_id");
|
||||||
|
|
||||||
|
b.Property<string>("CommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("commit_type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_by");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMyDay")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_my_day");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStarred")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_starred");
|
||||||
|
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("notes");
|
||||||
|
|
||||||
|
b.Property<string>("ParentTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("parent_task_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_finalized_at");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningPhase")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("none")
|
||||||
|
.HasColumnName("planning_phase");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_id");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionToken")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_token");
|
||||||
|
|
||||||
|
b.Property<string>("Result")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BlockedByTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_blocked_by");
|
||||||
|
|
||||||
|
b.HasIndex("ListId")
|
||||||
|
.HasDatabaseName("idx_tasks_list_id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_tasks_status");
|
||||||
|
|
||||||
|
b.HasIndex("ListId", "SortOrder")
|
||||||
|
.HasDatabaseName("idx_tasks_list_sort");
|
||||||
|
|
||||||
|
b.ToTable("tasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("error_markdown");
|
||||||
|
|
||||||
|
b.Property<int?>("ExitCode")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("exit_code");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRetry")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_retry");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<string>("Prompt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt");
|
||||||
|
|
||||||
|
b.Property<string>("ResultMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result_markdown");
|
||||||
|
|
||||||
|
b.Property<int>("RunNumber")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("run_number");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("StructuredOutputJson")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("structured_output");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensIn")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_in");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensOut")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_out");
|
||||||
|
|
||||||
|
b.Property<int?>("TurnCount")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("turn_count");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_runs_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_runs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("BaseCommit")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("BranchName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("branch_name");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DiffStat")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("diff_stat");
|
||||||
|
|
||||||
|
b.Property<string>("HeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("head_commit");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<string>("State")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("active")
|
||||||
|
.HasColumnName("state");
|
||||||
|
|
||||||
|
b.HasKey("TaskId");
|
||||||
|
|
||||||
|
b.ToTable("worktrees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithOne("Config")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Subtasks")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BlockedByTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithMany("Tasks")
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Runs")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithOne("Worktree")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Config");
|
||||||
|
|
||||||
|
b.Navigation("Tasks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("Runs");
|
||||||
|
|
||||||
|
b.Navigation("Subtasks");
|
||||||
|
|
||||||
|
b.Navigation("Worktree");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddMaxParallelExecutions : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "max_parallel_executions",
|
||||||
|
table: "app_settings",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 1);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "app_settings",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "max_parallel_executions",
|
||||||
|
value: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "max_parallel_executions",
|
||||||
|
table: "app_settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,12 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasDefaultValue("bypassPermissions")
|
.HasDefaultValue("bypassPermissions")
|
||||||
.HasColumnName("default_permission_mode");
|
.HasColumnName("default_permission_mode");
|
||||||
|
|
||||||
|
b.Property<int>("MaxParallelExecutions")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("max_parallel_executions");
|
||||||
|
|
||||||
b.Property<string>("RepoImportFolders")
|
b.Property<string>("RepoImportFolders")
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("repo_import_folders");
|
.HasColumnName("repo_import_folders");
|
||||||
@@ -89,6 +95,7 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
DefaultMaxTurns = 100,
|
DefaultMaxTurns = 100,
|
||||||
DefaultModel = "sonnet",
|
DefaultModel = "sonnet",
|
||||||
DefaultPermissionMode = "auto",
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
WorktreeAutoCleanupDays = 7,
|
WorktreeAutoCleanupDays = 7,
|
||||||
WorktreeAutoCleanupEnabled = false,
|
WorktreeAutoCleanupEnabled = false,
|
||||||
WorktreeStrategy = "sibling"
|
WorktreeStrategy = "sibling"
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ public sealed class AppSettingsEntity
|
|||||||
public int DefaultMaxTurns { get; set; } = 100;
|
public int DefaultMaxTurns { get; set; } = 100;
|
||||||
public string DefaultPermissionMode { get; set; } = "auto";
|
public string DefaultPermissionMode { get; set; } = "auto";
|
||||||
|
|
||||||
|
public int MaxParallelExecutions { get; set; } = 1;
|
||||||
|
|
||||||
public string WorktreeStrategy { get; set; } = "sibling";
|
public string WorktreeStrategy { get; set; } = "sibling";
|
||||||
public string? CentralWorktreeRoot { get; set; }
|
public string? CentralWorktreeRoot { get; set; }
|
||||||
public bool WorktreeAutoCleanupEnabled { get; set; }
|
public bool WorktreeAutoCleanupEnabled { get; set; }
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public sealed class AppSettingsRepository
|
|||||||
row.DefaultMaxTurns = updated.DefaultMaxTurns;
|
row.DefaultMaxTurns = updated.DefaultMaxTurns;
|
||||||
row.DefaultPermissionMode = string.IsNullOrWhiteSpace(updated.DefaultPermissionMode)
|
row.DefaultPermissionMode = string.IsNullOrWhiteSpace(updated.DefaultPermissionMode)
|
||||||
? "auto" : updated.DefaultPermissionMode;
|
? "auto" : updated.DefaultPermissionMode;
|
||||||
|
row.MaxParallelExecutions = updated.MaxParallelExecutions < 1 ? 1 : updated.MaxParallelExecutions;
|
||||||
row.WorktreeStrategy = string.IsNullOrWhiteSpace(updated.WorktreeStrategy) ? "sibling" : updated.WorktreeStrategy;
|
row.WorktreeStrategy = string.IsNullOrWhiteSpace(updated.WorktreeStrategy) ? "sibling" : updated.WorktreeStrategy;
|
||||||
row.CentralWorktreeRoot = string.IsNullOrWhiteSpace(updated.CentralWorktreeRoot)
|
row.CentralWorktreeRoot = string.IsNullOrWhiteSpace(updated.CentralWorktreeRoot)
|
||||||
? null : updated.CentralWorktreeRoot;
|
? null : updated.CentralWorktreeRoot;
|
||||||
|
|||||||
@@ -450,6 +450,7 @@ public sealed record AppSettingsDto(
|
|||||||
string DefaultModel,
|
string DefaultModel,
|
||||||
int DefaultMaxTurns,
|
int DefaultMaxTurns,
|
||||||
string DefaultPermissionMode,
|
string DefaultPermissionMode,
|
||||||
|
int MaxParallelExecutions,
|
||||||
string WorktreeStrategy,
|
string WorktreeStrategy,
|
||||||
string? CentralWorktreeRoot,
|
string? CentralWorktreeRoot,
|
||||||
bool WorktreeAutoCleanupEnabled,
|
bool WorktreeAutoCleanupEnabled,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private string _defaultModel = ModelRegistry.DefaultAlias;
|
[ObservableProperty] private string _defaultModel = ModelRegistry.DefaultAlias;
|
||||||
[ObservableProperty] private int _defaultMaxTurns = 100;
|
[ObservableProperty] private int _defaultMaxTurns = 100;
|
||||||
[ObservableProperty] private string _defaultPermissionMode = PermissionModeRegistry.DefaultMode;
|
[ObservableProperty] private string _defaultPermissionMode = PermissionModeRegistry.DefaultMode;
|
||||||
|
[ObservableProperty] private int _maxParallelExecutions = 1;
|
||||||
|
|
||||||
public IReadOnlyList<string> Models { get; } = ModelRegistry.Aliases;
|
public IReadOnlyList<string> Models { get; } = ModelRegistry.Aliases;
|
||||||
public IReadOnlyList<string> PermissionModes { get; } = PermissionModeRegistry.Modes;
|
public IReadOnlyList<string> PermissionModes { get; } = PermissionModeRegistry.Modes;
|
||||||
@@ -17,6 +18,8 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
if (DefaultMaxTurns < 1 || DefaultMaxTurns > 200)
|
if (DefaultMaxTurns < 1 || DefaultMaxTurns > 200)
|
||||||
return "Max turns must be between 1 and 200.";
|
return "Max turns must be between 1 and 200.";
|
||||||
|
if (MaxParallelExecutions < 1 || MaxParallelExecutions > 20)
|
||||||
|
return "Max parallel executions must be between 1 and 20.";
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
General.DefaultModel = dto.DefaultModel ?? "sonnet";
|
General.DefaultModel = dto.DefaultModel ?? "sonnet";
|
||||||
General.DefaultMaxTurns = dto.DefaultMaxTurns;
|
General.DefaultMaxTurns = dto.DefaultMaxTurns;
|
||||||
General.DefaultPermissionMode = dto.DefaultPermissionMode ?? "auto";
|
General.DefaultPermissionMode = dto.DefaultPermissionMode ?? "auto";
|
||||||
|
General.MaxParallelExecutions = dto.MaxParallelExecutions;
|
||||||
Worktrees.WorktreeStrategy = dto.WorktreeStrategy ?? "sibling";
|
Worktrees.WorktreeStrategy = dto.WorktreeStrategy ?? "sibling";
|
||||||
Worktrees.CentralWorktreeRoot = dto.CentralWorktreeRoot;
|
Worktrees.CentralWorktreeRoot = dto.CentralWorktreeRoot;
|
||||||
Worktrees.WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled;
|
Worktrees.WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled;
|
||||||
@@ -69,6 +70,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
General.DefaultModel ?? "sonnet",
|
General.DefaultModel ?? "sonnet",
|
||||||
General.DefaultMaxTurns,
|
General.DefaultMaxTurns,
|
||||||
General.DefaultPermissionMode ?? "auto",
|
General.DefaultPermissionMode ?? "auto",
|
||||||
|
General.MaxParallelExecutions,
|
||||||
Worktrees.WorktreeStrategy ?? "sibling",
|
Worktrees.WorktreeStrategy ?? "sibling",
|
||||||
string.IsNullOrWhiteSpace(Worktrees.CentralWorktreeRoot) ? null : Worktrees.CentralWorktreeRoot,
|
string.IsNullOrWhiteSpace(Worktrees.CentralWorktreeRoot) ? null : Worktrees.CentralWorktreeRoot,
|
||||||
Worktrees.WorktreeAutoCleanupEnabled,
|
Worktrees.WorktreeAutoCleanupEnabled,
|
||||||
|
|||||||
@@ -75,6 +75,14 @@
|
|||||||
HorizontalAlignment="Stretch"/>
|
HorizontalAlignment="Stretch"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="Max parallel executions"/>
|
||||||
|
<NumericUpDown Value="{Binding General.MaxParallelExecutions, Mode=TwoWay}"
|
||||||
|
Minimum="1" Maximum="20" Increment="1" FormatString="0"
|
||||||
|
HorizontalAlignment="Left" Width="140"/>
|
||||||
|
<TextBlock Text="How many queued tasks the worker runs at once."
|
||||||
|
Opacity="0.6" FontSize="12"/>
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public record AppSettingsDto(
|
|||||||
string DefaultModel,
|
string DefaultModel,
|
||||||
int DefaultMaxTurns,
|
int DefaultMaxTurns,
|
||||||
string DefaultPermissionMode,
|
string DefaultPermissionMode,
|
||||||
|
int MaxParallelExecutions,
|
||||||
string WorktreeStrategy,
|
string WorktreeStrategy,
|
||||||
string? CentralWorktreeRoot,
|
string? CentralWorktreeRoot,
|
||||||
bool WorktreeAutoCleanupEnabled,
|
bool WorktreeAutoCleanupEnabled,
|
||||||
@@ -202,6 +203,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
row.DefaultModel,
|
row.DefaultModel,
|
||||||
row.DefaultMaxTurns,
|
row.DefaultMaxTurns,
|
||||||
row.DefaultPermissionMode,
|
row.DefaultPermissionMode,
|
||||||
|
row.MaxParallelExecutions,
|
||||||
row.WorktreeStrategy,
|
row.WorktreeStrategy,
|
||||||
row.CentralWorktreeRoot,
|
row.CentralWorktreeRoot,
|
||||||
row.WorktreeAutoCleanupEnabled,
|
row.WorktreeAutoCleanupEnabled,
|
||||||
@@ -219,6 +221,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
DefaultModel = dto.DefaultModel ?? ModelRegistry.DefaultAlias,
|
DefaultModel = dto.DefaultModel ?? ModelRegistry.DefaultAlias,
|
||||||
DefaultMaxTurns = dto.DefaultMaxTurns,
|
DefaultMaxTurns = dto.DefaultMaxTurns,
|
||||||
DefaultPermissionMode = dto.DefaultPermissionMode ?? PermissionModeRegistry.DefaultMode,
|
DefaultPermissionMode = dto.DefaultPermissionMode ?? PermissionModeRegistry.DefaultMode,
|
||||||
|
MaxParallelExecutions = dto.MaxParallelExecutions,
|
||||||
WorktreeStrategy = dto.WorktreeStrategy ?? "sibling",
|
WorktreeStrategy = dto.WorktreeStrategy ?? "sibling",
|
||||||
CentralWorktreeRoot = dto.CentralWorktreeRoot,
|
CentralWorktreeRoot = dto.CentralWorktreeRoot,
|
||||||
WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled,
|
WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public sealed class QueueService : BackgroundService
|
|||||||
private readonly OverrideSlotService _override;
|
private readonly OverrideSlotService _override;
|
||||||
|
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
private volatile QueueSlotState? _queueSlot;
|
private readonly Dictionary<string, QueueSlotState> _queueSlots = new();
|
||||||
|
|
||||||
public QueueService(
|
public QueueService(
|
||||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||||
@@ -41,8 +41,11 @@ public sealed class QueueService : BackgroundService
|
|||||||
public IReadOnlyList<(string slot, string taskId, DateTime startedAt)> GetActive()
|
public IReadOnlyList<(string slot, string taskId, DateTime startedAt)> GetActive()
|
||||||
{
|
{
|
||||||
var list = new List<(string, string, DateTime)>();
|
var list = new List<(string, string, DateTime)>();
|
||||||
var q = _queueSlot;
|
lock (_lock)
|
||||||
if (q is not null) list.Add(("queue", q.TaskId, q.StartedAt));
|
{
|
||||||
|
foreach (var slot in _queueSlots.Values)
|
||||||
|
list.Add(("queue", slot.TaskId, slot.StartedAt));
|
||||||
|
}
|
||||||
var o = _override.CurrentSlot;
|
var o = _override.CurrentSlot;
|
||||||
if (o is not null) list.Add(("override", o.TaskId, o.StartedAt));
|
if (o is not null) list.Add(("override", o.TaskId, o.StartedAt));
|
||||||
return list;
|
return list;
|
||||||
@@ -64,7 +67,7 @@ public sealed class QueueService : BackgroundService
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (_queueSlot?.TaskId == taskId)
|
if (_queueSlots.ContainsKey(taskId))
|
||||||
throw new InvalidOperationException("task is already running in queue slot");
|
throw new InvalidOperationException("task is already running in queue slot");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,9 +78,9 @@ public sealed class QueueService : BackgroundService
|
|||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (_queueSlot is not null && _queueSlot.TaskId == taskId)
|
if (_queueSlots.TryGetValue(taskId, out var slot))
|
||||||
{
|
{
|
||||||
_queueSlot.Cts.Cancel();
|
slot.Cts.Cancel();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,28 +103,35 @@ public sealed class QueueService : BackgroundService
|
|||||||
|
|
||||||
await Task.WhenAny(wakeTask, timerTask);
|
await Task.WhenAny(wakeTask, timerTask);
|
||||||
|
|
||||||
if (_queueSlot is not null) continue;
|
var maxParallel = await GetMaxParallelAsync(stoppingToken);
|
||||||
|
|
||||||
|
// Fill as many free slots as the limit allows.
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_queueSlots.Count >= maxParallel) break;
|
||||||
|
}
|
||||||
|
|
||||||
var task = await _picker.ClaimNextAsync(DateTime.UtcNow, stoppingToken);
|
var task = await _picker.ClaimNextAsync(DateTime.UtcNow, stoppingToken);
|
||||||
if (task is null) continue;
|
if (task is null) break;
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (_queueSlot is not null) continue;
|
|
||||||
|
|
||||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
|
var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
|
||||||
_queueSlot = new QueueSlotState { TaskId = task.Id, StartedAt = DateTime.UtcNow, Cts = cts };
|
_queueSlots[task.Id] = new QueueSlotState { TaskId = task.Id, StartedAt = DateTime.UtcNow, Cts = cts };
|
||||||
|
|
||||||
_ = RunInSlotAsync(task.Id, cts.Token).ContinueWith(t =>
|
_ = RunInSlotAsync(task.Id, cts.Token).ContinueWith(t =>
|
||||||
{
|
{
|
||||||
if (t.IsFaulted)
|
if (t.IsFaulted)
|
||||||
_logger.LogError(t.Exception, "RunInSlotAsync failed for task {TaskId} in queue slot", task.Id);
|
_logger.LogError(t.Exception, "RunInSlotAsync failed for task {TaskId} in queue slot", task.Id);
|
||||||
lock (_lock) { _queueSlot = null; }
|
lock (_lock) { _queueSlots.Remove(task.Id); }
|
||||||
cts.Dispose();
|
cts.Dispose();
|
||||||
_waker.Wake(); // Check for next task immediately.
|
_waker.Wake(); // Check for next task immediately.
|
||||||
}, TaskScheduler.Default);
|
}, TaskScheduler.Default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -135,6 +145,21 @@ public sealed class QueueService : BackgroundService
|
|||||||
_logger.LogInformation("QueueService stopping");
|
_logger.LogInformation("QueueService stopping");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<int> GetMaxParallelAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = _dbFactory.CreateDbContext();
|
||||||
|
var settings = await new AppSettingsRepository(context).GetAsync(ct);
|
||||||
|
return Math.Max(1, settings.MaxParallelExecutions);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to read max parallel executions; defaulting to 1");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RunInSlotAsync(string taskId, CancellationToken ct)
|
private async Task RunInSlotAsync(string taskId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
Reference in New Issue
Block a user