Skip to content
Merged
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
39 changes: 39 additions & 0 deletions Resgrid.sln
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Resgrid.Chatbot.NLU", "Core
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Resgrid.Providers.Chatbot", "Providers\Resgrid.Providers.Chatbot\Resgrid.Providers.Chatbot.csproj", "{D3E4F5A6-B7C8-9012-CDEF-012345678902}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quidjibo.SqlServer", "Workers\Support\Quidjibo.SqlServer\Quidjibo.SqlServer.csproj", "{93931385-3360-455F-B051-CF7B56C0ED17}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Azure|Any CPU = Azure|Any CPU
Expand Down Expand Up @@ -1496,6 +1498,42 @@ Global
{D3E4F5A6-B7C8-9012-CDEF-012345678902}.Staging|x86.Build.0 = Debug|Any CPU
{D3E4F5A6-B7C8-9012-CDEF-012345678902}.Staging|x64.ActiveCfg = Debug|Any CPU
{D3E4F5A6-B7C8-9012-CDEF-012345678902}.Staging|x64.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Azure|Any CPU.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Azure|Any CPU.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Azure|x86.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Azure|x86.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Azure|x64.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Azure|x64.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Cloud|Any CPU.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Cloud|Any CPU.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Cloud|x86.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Cloud|x86.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Cloud|x64.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Cloud|x64.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Debug|Any CPU.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Debug|x86.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Debug|x86.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Debug|x64.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Debug|x64.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Docker|Any CPU.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Docker|Any CPU.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Docker|x86.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Docker|x86.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Docker|x64.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Docker|x64.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Release|Any CPU.ActiveCfg = Release|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Release|Any CPU.Build.0 = Release|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Release|x86.ActiveCfg = Release|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Release|x86.Build.0 = Release|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Release|x64.ActiveCfg = Release|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Release|x64.Build.0 = Release|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Staging|Any CPU.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Staging|Any CPU.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Staging|x86.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Staging|x86.Build.0 = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Staging|x64.ActiveCfg = Debug|Any CPU
{93931385-3360-455F-B051-CF7B56C0ED17}.Staging|x64.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1540,6 +1578,7 @@ Global
{B1A2C3D4-E5F6-7890-ABCD-EF1234567890} = {D43D1D6B-66A9-4A57-9EA3-8DECC92FA583}
{C2D3E4F5-A6B7-8901-BCDE-F12345678901} = {D43D1D6B-66A9-4A57-9EA3-8DECC92FA583}
{D3E4F5A6-B7C8-9012-CDEF-012345678902} = {F06D475C-635C-4DE4-82BA-C49A90BA8FCD}
{93931385-3360-455F-B051-CF7B56C0ED17} = {89331D76-C527-479D-8F30-8033A04C625F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {156116FF-243E-45E8-8717-DB72E95F56AF}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
<PackageReference Include="Quidjibo" Version="0.6.0" />
<PackageReference Include="Quidjibo.Autofac" Version="0.6.0" />
<PackageReference Include="Quidjibo.DataProtection" Version="0.6.0" />
<PackageReference Include="Quidjibo.SqlServer" Version="0.6.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.3" />
Expand Down Expand Up @@ -58,6 +57,7 @@
<ProjectReference Include="..\..\Repositories\Resgrid.Repositories.DataRepository\Resgrid.Repositories.DataRepository.csproj" />
<ProjectReference Include="..\Resgrid.Workers.Framework\Resgrid.Workers.Framework.csproj" />
<ProjectReference Include="..\Support\Quidjibo.Postgres\Quidjibo.Postgres.csproj" />
<ProjectReference Include="..\Support\Quidjibo.SqlServer\Quidjibo.SqlServer.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="deps\" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ public PostgresProgressProviderFactory(
return _provider;
}

await SyncLock.WaitAsync(cancellationToken);
try
{
await SyncLock.WaitAsync(cancellationToken);
await PostgresRunner.ExecuteAsync(async cmd =>
{
cmd.CommandText = await SqlLoader.GetScript("Progress.Setup");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ public PostgresScheduleProviderFactory(

public async Task<IScheduleProvider> CreateAsync(string[] queues, CancellationToken cancellationToken = default(CancellationToken))
{
await SyncLock.WaitAsync(cancellationToken);
try
{
await SyncLock.WaitAsync(cancellationToken);
await PostgresRunner.ExecuteAsync(async cmd =>
{
var schemaSetup = await SqlLoader.GetScript("Schema.Setup");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ public PostgresWorkProviderFactory(

public async Task<IWorkProvider> CreateAsync(string[] queues, CancellationToken cancellationToken = default(CancellationToken))
{
await SyncLock.WaitAsync(cancellationToken);
try
{
await SyncLock.WaitAsync(cancellationToken);
if (!_initialized)
{
await PostgresRunner.ExecuteAsync(async cmd =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// // Copyright (c) smiggleworth. All rights reserved.
// // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Quidjibo.Configurations;
using Quidjibo.Constants;

namespace Quidjibo.SqlServer.Configurations
{
public class SqlServerQuidjiboConfiguration : IQuidjiboConfiguration
{
public int PollingInterval { get; set; } = 10;
public string ConnectionString { get; set; }

/// <summary>
/// The number of days to keep completed/faulted work items.
/// </summary>
public int DaysToKeep { get; set; } = 3;

public int BatchSize { get; set; } = 5;

/// <inheritdoc />
public int? WorkPollingInterval { get; set; }

/// <inheritdoc />
public bool EnableScheduler { get; set; } = true;

/// <inheritdoc />
public int? SchedulePollingInterval { get; set; }

/// <inheritdoc />
public bool EnableWorker { get; set; } = true;

/// <inheritdoc />
public bool SingleLoop { get; set; } = true;

/// <inheritdoc />
public int LockInterval { get; set; } = 30;

/// <inheritdoc />
public int MaxAttempts { get; set; } = 5;

/// <inheritdoc />
public int Throttle { get; set; } = 10;

/// <inheritdoc />
public string[] Queues { get; set; } = Default.Queues;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System;
using Quidjibo.Constants;
using Quidjibo.SqlServer.Configurations;
using Quidjibo.SqlServer.Factories;

namespace Quidjibo.SqlServer.Extensions
{
public static class QuidjiboBuilderExtensions
{
/// <summary>
/// Use Sql Server for Work, Progress, an Scheduled Jobs
/// </summary>
/// <param name="builder"></param>
/// <param name="sqlServerQuidjiboConfiguration"></param>
/// <returns></returns>
public static QuidjiboBuilder UseSqlServer(this QuidjiboBuilder builder, Action<SqlServerQuidjiboConfiguration> sqlServerQuidjiboConfiguration)
{
var config = new SqlServerQuidjiboConfiguration();
sqlServerQuidjiboConfiguration(config);
return builder.UseSqlServer(config);
Comment on lines +16 to +20

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail fast on null configuration and blank connection strings.

These entry points pass configuration straight into the factories, so a missing delegate/config/connection string is only discovered later when the provider first opens SQL connections. Validate the public inputs here so startup fails deterministically.

Proposed fix
         public static QuidjiboBuilder UseSqlServer(this QuidjiboBuilder builder, Action<SqlServerQuidjiboConfiguration> sqlServerQuidjiboConfiguration)
         {
+            ArgumentNullException.ThrowIfNull(builder);
+            ArgumentNullException.ThrowIfNull(sqlServerQuidjiboConfiguration);
+
             var config = new SqlServerQuidjiboConfiguration();
             sqlServerQuidjiboConfiguration(config);
             return builder.UseSqlServer(config);
         }
@@
         public static QuidjiboBuilder UseSqlServer(this QuidjiboBuilder builder, SqlServerQuidjiboConfiguration sqlServerQuidjiboConfiguration)
         {
+            ArgumentNullException.ThrowIfNull(builder);
+            ArgumentNullException.ThrowIfNull(sqlServerQuidjiboConfiguration);
+            if (string.IsNullOrWhiteSpace(sqlServerQuidjiboConfiguration.ConnectionString))
+                throw new ArgumentException("ConnectionString is required.", nameof(sqlServerQuidjiboConfiguration));
+
             return builder.Configure(sqlServerQuidjiboConfiguration)
                           .ConfigureWorkProviderFactory(new SqlWorkProviderFactory(builder.LoggerFactory, sqlServerQuidjiboConfiguration))
                           .ConfigureProgressProviderFactory(new SqlProgressProviderFactory(builder.LoggerFactory, sqlServerQuidjiboConfiguration.ConnectionString))
                           .ConfigureScheduleProviderFactory(new SqlScheduleProviderFactory(builder.LoggerFactory, sqlServerQuidjiboConfiguration.ConnectionString));
         }
@@
         public static QuidjiboBuilder UseSqlServer(this QuidjiboBuilder builder, string connectionString, params string[] queues)
         {
+            ArgumentNullException.ThrowIfNull(builder);
+            if (string.IsNullOrWhiteSpace(connectionString))
+                throw new ArgumentException("Connection string is required.", nameof(connectionString));
+
             if (queues == null || queues.Length == 0)
             {
                 queues = Default.Queues;
             }
@@
         public static QuidjiboBuilder UseSqlServerForWork(this QuidjiboBuilder builder, SqlServerQuidjiboConfiguration sqlServerQuidjiboConfiguration)
         {
+            ArgumentNullException.ThrowIfNull(builder);
+            ArgumentNullException.ThrowIfNull(sqlServerQuidjiboConfiguration);
+            if (string.IsNullOrWhiteSpace(sqlServerQuidjiboConfiguration.ConnectionString))
+                throw new ArgumentException("ConnectionString is required.", nameof(sqlServerQuidjiboConfiguration));
+
             return builder.ConfigureWorkProviderFactory(new SqlWorkProviderFactory(builder.LoggerFactory, sqlServerQuidjiboConfiguration));
         }
@@
         public static QuidjiboBuilder UseSqlServerForProgress(this QuidjiboBuilder builder, string connectionString)
         {
+            ArgumentNullException.ThrowIfNull(builder);
+            if (string.IsNullOrWhiteSpace(connectionString))
+                throw new ArgumentException("Connection string is required.", nameof(connectionString));
+
             return builder.ConfigureProgressProviderFactory(new SqlProgressProviderFactory(builder.LoggerFactory, connectionString));
         }
@@
         public static QuidjiboBuilder UseSqlServerForSchedule(this QuidjiboBuilder builder, string connectionString)
         {
+            ArgumentNullException.ThrowIfNull(builder);
+            if (string.IsNullOrWhiteSpace(connectionString))
+                throw new ArgumentException("Connection string is required.", nameof(connectionString));
+
             return builder.ConfigureScheduleProviderFactory(new SqlScheduleProviderFactory(builder.LoggerFactory, connectionString));
         }

Also applies to: 29-35, 44-60, 68-71, 79-93

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Workers/Support/Quidjibo.SqlServer/Extensions/QuidjiboBuilderExtensions.cs`
around lines 16 - 20, The Sql Server builder entry points are accepting
null/blank inputs and deferring failures until connection time; add eager
validation in the public extension methods so startup fails immediately. In
QuidjiboBuilderExtensions, validate the configuration delegate/config object
before using it, and check the connection string for null/whitespace in the
UseSqlServer overloads and any related setup methods referenced by the comment.
Use the existing symbols like UseSqlServer and SqlServerQuidjiboConfiguration to
locate all affected entry points and throw an argument validation failure before
passing values into the factories.

}

/// <summary>
/// Use Sql Server for Work, Progress, and Scheduled Jobs
/// </summary>
/// <param name="builder"></param>
/// <param name="sqlServerQuidjiboConfiguration"></param>
/// <returns></returns>
public static QuidjiboBuilder UseSqlServer(this QuidjiboBuilder builder, SqlServerQuidjiboConfiguration sqlServerQuidjiboConfiguration)
{
return builder.Configure(sqlServerQuidjiboConfiguration)
.ConfigureWorkProviderFactory(new SqlWorkProviderFactory(builder.LoggerFactory, sqlServerQuidjiboConfiguration))
.ConfigureProgressProviderFactory(new SqlProgressProviderFactory(builder.LoggerFactory, sqlServerQuidjiboConfiguration.ConnectionString))
.ConfigureScheduleProviderFactory(new SqlScheduleProviderFactory(builder.LoggerFactory, sqlServerQuidjiboConfiguration.ConnectionString));
}

/// <summary>
/// Use Sql Server for Work, Progress, and Scheduled Jobs
/// </summary>
/// <param name="builder"></param>
/// <param name="connectionString"></param>
/// <param name="queues"></param>
/// <returns></returns>
public static QuidjiboBuilder UseSqlServer(this QuidjiboBuilder builder, string connectionString, params string[] queues)
{
if (queues == null || queues.Length == 0)
{
queues = Default.Queues;
}

var config = new SqlServerQuidjiboConfiguration
{
ConnectionString = connectionString,
Queues = queues
};
return builder.Configure(config)
.ConfigureWorkProviderFactory(new SqlWorkProviderFactory(builder.LoggerFactory, config))
.ConfigureProgressProviderFactory(new SqlProgressProviderFactory(builder.LoggerFactory, connectionString))
.ConfigureScheduleProviderFactory(new SqlScheduleProviderFactory(builder.LoggerFactory, connectionString));
}

/// <summary>
/// Use Sql Server For Work
/// </summary>
/// <param name="builder"></param>
/// <param name="sqlServerQuidjiboConfiguration"></param>
/// <returns></returns>
public static QuidjiboBuilder UseSqlServerForWork(this QuidjiboBuilder builder, SqlServerQuidjiboConfiguration sqlServerQuidjiboConfiguration)
{
return builder.ConfigureWorkProviderFactory(new SqlWorkProviderFactory(builder.LoggerFactory, sqlServerQuidjiboConfiguration));
}
Comment on lines +68 to +71

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

UseSqlServerForWork drops the caller's configuration.

Unlike the full UseSqlServer(...) overloads, this method only swaps the work-provider factory. The passed SqlServerQuidjiboConfiguration never gets applied to the builder, so builder-level settings from that object (PollingInterval, WorkPollingInterval, EnableWorker, SingleLoop, Throttle, etc.) are ignored.

Proposed fix
         public static QuidjiboBuilder UseSqlServerForWork(this QuidjiboBuilder builder, SqlServerQuidjiboConfiguration sqlServerQuidjiboConfiguration)
         {
-            return builder.ConfigureWorkProviderFactory(new SqlWorkProviderFactory(builder.LoggerFactory, sqlServerQuidjiboConfiguration));
+            return builder.Configure(sqlServerQuidjiboConfiguration)
+                          .ConfigureWorkProviderFactory(new SqlWorkProviderFactory(builder.LoggerFactory, sqlServerQuidjiboConfiguration));
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static QuidjiboBuilder UseSqlServerForWork(this QuidjiboBuilder builder, SqlServerQuidjiboConfiguration sqlServerQuidjiboConfiguration)
{
return builder.ConfigureWorkProviderFactory(new SqlWorkProviderFactory(builder.LoggerFactory, sqlServerQuidjiboConfiguration));
}
public static QuidjiboBuilder UseSqlServerForWork(this QuidjiboBuilder builder, SqlServerQuidjiboConfiguration sqlServerQuidjiboConfiguration)
{
return builder.Configure(sqlServerQuidjiboConfiguration)
.ConfigureWorkProviderFactory(new SqlWorkProviderFactory(builder.LoggerFactory, sqlServerQuidjiboConfiguration));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Workers/Support/Quidjibo.SqlServer/Extensions/QuidjiboBuilderExtensions.cs`
around lines 68 - 71, The UseSqlServerForWork method is only replacing the work
provider factory and never applies the provided SqlServerQuidjiboConfiguration
to the QuidjiboBuilder, so caller-supplied builder settings are ignored. Update
UseSqlServerForWork in QuidjiboBuilderExtensions to also bind/apply the passed
configuration the same way the full UseSqlServer overloads do, while still
configuring SqlWorkProviderFactory, so settings like PollingInterval,
WorkPollingInterval, EnableWorker, SingleLoop, and Throttle are honored.


/// <summary>
/// Use Sql Server For Progress
/// </summary>
/// <param name="builder"></param>
/// <param name="connectionString"></param>
/// <returns></returns>
public static QuidjiboBuilder UseSqlServerForProgress(this QuidjiboBuilder builder, string connectionString)
{
return builder.ConfigureProgressProviderFactory(new SqlProgressProviderFactory(builder.LoggerFactory, connectionString));
}

/// <summary>
/// Use Sql Server For Scheduled Jobs
/// </summary>
/// <param name="builder"></param>
/// <param name="connectionString"></param>
/// <returns></returns>
public static QuidjiboBuilder UseSqlServerForSchedule(this QuidjiboBuilder builder, string connectionString)
{
return builder.ConfigureScheduleProviderFactory(new SqlScheduleProviderFactory(builder.LoggerFactory, connectionString));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) smiggleworth. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.Data.SqlClient;
using System.Threading;
using System.Threading.Tasks;
using Quidjibo.Models;
using Quidjibo.SqlServer.Providers;
using Quidjibo.SqlServer.Utils;

namespace Quidjibo.SqlServer.Extensions
{
public static class SqlCommandExtensions
{
public static async Task PrepareForSendAsync(this SqlCommand cmd, WorkItem item, int delay, CancellationToken cancellationToken)
{
var createdOn = DateTime.UtcNow;
var visibleOn = createdOn.AddSeconds(delay);
var expireOn = item.ExpireOn == default(DateTime) ? visibleOn.AddDays(SqlWorkProvider.DEFAULT_EXPIRE_DAYS) : item.ExpireOn;

#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
cmd.CommandText = await SqlLoader.GetScript("Work.Send");
#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities
cmd.AddParameter("@Id", item.Id);
cmd.AddParameter("@ScheduleId", item.ScheduleId);
cmd.AddParameter("@CorrelationId", item.CorrelationId);
cmd.AddParameter("@Name", item.Name);
cmd.AddParameter("@Worker", item.Worker);
cmd.AddParameter("@Queue", item.Queue);
cmd.AddParameter("@Attempts", item.Attempts);
cmd.AddParameter("@CreatedOn", createdOn);
cmd.AddParameter("@ExpireOn", expireOn);
cmd.AddParameter("@VisibleOn", visibleOn);
cmd.AddParameter("@Status", SqlWorkProvider.StatusFlags.New);
cmd.AddParameter("@Payload", item.Payload);
}

public static async Task PrepareForRenewAsync(this SqlCommand cmd, WorkItem item, DateTime lockExpireOn, CancellationToken cancellationToken)
{
#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
cmd.CommandText = await SqlLoader.GetScript("Work.Renew");
#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities
cmd.AddParameter("@Id", item.Id);
cmd.AddParameter("@VisibleOn", lockExpireOn);
}

public static async Task PrepareForCompleteAsync(this SqlCommand cmd, WorkItem item, CancellationToken cancellationToken)
{
#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
cmd.CommandText = await SqlLoader.GetScript("Work.Complete");
#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities
cmd.AddParameter("@Id", item.Id);
cmd.AddParameter("@Complete", SqlWorkProvider.StatusFlags.Complete);
}

public static async Task PrepareForFaultAsync(this SqlCommand cmd, WorkItem item, int visibilityTimeout, CancellationToken cancellationToken)
{
var faultedOn = DateTime.UtcNow;

#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
cmd.CommandText = await SqlLoader.GetScript("Work.Fault");
#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities
cmd.AddParameter("@Id", item.Id);
cmd.AddParameter("@VisibleOn", faultedOn.AddSeconds(Math.Max(visibilityTimeout, 30)));
cmd.AddParameter("@Faulted", SqlWorkProvider.StatusFlags.Faulted);
}
Comment on lines +39 to +67

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Include a lease owner/token in work state transitions.

Renew, complete, and fault currently prepare updates using only @Id. A stale worker can still update a job after its visibility timeout expires and another worker receives the same row. Pass a receiver-specific token/worker value into these commands and have the SQL WHERE clause enforce it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Workers/Support/Quidjibo.SqlServer/Extensions/SqlCommandExtensions.cs` around
lines 39 - 67, The work transition helpers in SqlCommandExtensions currently
rely only on `@Id`, which allows stale workers to update rows after lease
transfer. Update PrepareForRenewAsync, PrepareForCompleteAsync, and
PrepareForFaultAsync to accept and add a lease owner/token or worker-specific
value, then ensure the corresponding SQL scripts use that value in the WHERE
clause so only the current owner can renew, complete, or fault a work item.


}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using Microsoft.Data.SqlClient;

namespace Quidjibo.SqlServer.Extensions
{
public static class SqlParameterCollectionExtensions
{
public static SqlParameter AddParameter(this SqlCommand cmd, string name, object value)
{
return cmd.Parameters.AddWithValue(name, value ?? DBNull.Value);
}
}
}
Loading
Loading