Skip to content

Update 2025.12.1#70

Open
azhe403 wants to merge 57 commits into
productionfrom
main
Open

Update 2025.12.1#70
azhe403 wants to merge 57 commits into
productionfrom
main

Conversation

@azhe403

@azhe403 azhe403 commented Dec 29, 2025

Copy link
Copy Markdown
Owner

@sonarqubecloud

Copy link
Copy Markdown

- Expand AGENTS.md with Cortex mediator, config merge order, and agent guidance
- Add .serena/memories and docs/code-wiki/agents.md
- Remove .windsurfrules
@sonarqubecloud

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

@devin-ai-integration devin-ai-integration Bot Jun 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Automatic note-finding post-processor was removed without replacement

The old FindNoteRequestHandler (backend/ZiziBot.Application/Handlers/Telegram/Note/FindNoteRequest.cs, deleted) was a IRequestPostProcessor that ran after every Telegram bot request. It automatically detected notes in messages — either via hashtag (#tag) or by matching the full message text against saved notes — and sent the matching note content back to the chat. This was a core bot feature.

In the new pipeline registration at backend/ZiziBot.Application/Infrastructure/Extensions/CortexExtension.cs:24-28, there is no equivalent ITelegramPostProcessPipeline registered for note finding. The feature appears to have been dropped during the migration from MediatR to Cortex.Mediator. Users who relied on hashtag-triggered notes will no longer receive automatic note responses.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +23 to +33
try
{
await ApplyActionsAsync(botRequest, actions, htmlMessage);

return PreProcessResult<TResponse>.Stop();
}
catch (Exception exception)
{
logger.LogError(exception, "Error on Action Result pipeline");
return PreProcessResult<TResponse>.Stop();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 ActionResultPipeline unconditionally short-circuits pipeline, preventing handler execution

The new ActionResultPipeline always returns PreProcessResult<TResponse>.Stop() (lines 27 and 32), which prevents the actual request handler from ever executing when pipeline actions are present. The old ActionResultPipelineBehavior (deleted in this PR) called return await next(cancellationToken) after applying actions, meaning the handler still processed the request. This is a significant behavioral regression: commands from users who trigger enforcement actions (e.g., users without a username receiving Mute+Warn from CheckUsernamePipeline) will now silently be dropped instead of being processed with a warning applied.

Prompt for agents
The ActionResultPipeline.ProcessAsync method always returns PreProcessResult<TResponse>.Stop() in both the try and catch blocks. The old equivalent code (ActionResultPipelineBehavior in backend/ZiziBot.Application/Handlers/Telegram/Middleware/ActionResultPipelineBehavior.cs) called 'return await next(cancellationToken)' after applying enforcement actions, meaning the handler still executed. The new code should return PreProcessResult<TResponse>.Continue in the normal (try) path after applying actions, and only Stop() on exception. This would preserve the original behavior where actions (delete, mute, kick, warn) are applied but the request still proceeds to the handler.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

"ShortCircuited"
);

return result.Response!;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 PreProcessPipeline returns null when Stop() is called without a response value

At PreProcessPipeline.cs:68, when a pre-processor short-circuits with !result.ShouldContinue, the pipeline returns result.Response!. However, PreProcessResult<TResponse>.Stop() (the parameterless overload at IPreProcessPipeline.cs:30-33) sets Response = default(TResponse), which is null for reference types like BotResponseBase. The ! operator suppresses the compiler warning but does not prevent a null return at runtime. Both ActionResultPipeline (lines 27, 32) and FeatureControlPipeline (line 35) use the parameterless Stop() variant, meaning the pipeline will return null to callers that expect a non-null TResponse.

Prompt for agents
In PreProcessPipeline.cs line 68, 'return result.Response!' can return null because PreProcessResult<TResponse>.Stop() sets Response to default(TResponse). Callers that use the parameterless Stop() (ActionResultPipeline lines 27/32, FeatureControlPipeline line 35) will cause null to be returned as the pipeline result. Fix options: (1) Add a null check and return a default-constructed TResponse (e.g. via Activator.CreateInstance or a 'new()' constraint) when result.Response is null, or (2) require all Stop() calls to provide a valid response instance, removing the parameterless Stop() overload.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 33 to +43
{
logger.LogError(e, "An error occured when executing task {Task}", task);
}
}, stoppingToken);

logger.LogInformation("Task triggered: {Task}", task);
}
logger.LogInformation("Task triggered: {Task}", task);
}

await Task.Delay(0, stoppingToken);
await Task.Delay(0, stoppingToken);

logger.LogInformation("All tasks are triggered. Count: {Count}", services.Count);
logger.LogInformation("All tasks are triggered. Count: {Count}", services.Count);
});

@devin-ai-integration devin-ai-integration Bot Jun 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: TaskRunnerHostedService changed from parallel to sequential task execution

The old TaskRunnerHostedService used Task.Run to fire-and-forget each startup task in parallel. The new implementation at backend/ZiziBot.Application/Services/HostedServices/TaskRunnerHostedService.cs:16-43 executes tasks sequentially within a single ExecuteAsync call via scopedUtil.ExecuteAsync. Each task runs to completion before the next one starts. This changes startup behavior: if any task is slow (e.g., RSS job registration, shalat time registration), it blocks subsequent tasks. The old parallel approach had its own issues (unobserved exceptions), but the sequential approach may significantly increase startup time.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 35 to +49
await dataFacade.MongoDb.SaveChangesAsync(cancellationToken);
}

var engineConfig = await dataFacade.AppSetting.GetRequiredConfigSectionAsync<EngineConfig>();
if (engineConfig.ConsoleUrl.IsNullOrWhiteSpace())
await serviceFacade.TelegramService.SendMessageText("Maaf fitur ini belum dipersiapkan");

var sessionId = Guid.NewGuid().ToString();
var webUrl = engineConfig.ConsoleUrl.SetQueryParam("session_id", sessionId).ToString();

var replyMarkup = InlineKeyboardMarkup.Empty();
var htmlMessage = HtmlMessage.Empty
.BoldBr("🎛 ZiziBot Console")
.TextBr("Buka Console untuk mengelola pengaturan, catatan dan lain-lain.")
.TextBr($"OTP: {otp}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 PrepareConsoleRequest: OTP generated and displayed regardless of chat type, but only persisted for private chats

The OTP is generated unconditionally on line 21 and displayed in the message on line 49 (TextBr($"OTP: {otp}")), but it is only saved to the database inside the if (request.IsPrivateChat) block (lines 23-36). In group chats, users will see a random OTP number that cannot be redeemed since it was never persisted. The old code only generated the OTP inside the private chat block. This could confuse group chat users who see an OTP they can't use.

(Refers to lines 21-49)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 new potential issues.

Open in Devin Review

Comment on lines +24 to +28
services.AddTransient(typeof(ITelegramPostProcessPipeline<,>), typeof(CheckAfkSessionPipeline<,>));
services.AddTransient(typeof(ITelegramPostProcessPipeline<,>), typeof(EnsureChatAdminPipeline<,>));
services.AddTransient(typeof(ITelegramPostProcessPipeline<,>), typeof(EnsureChatSettingPipeline<,>));
services.AddTransient(typeof(ITelegramPostProcessPipeline<,>), typeof(UpsertBotUserPipeline<,>));
services.AddTransient(typeof(ITelegramPostProcessPipeline<,>), typeof(InsertChatActivityPipeline<,>));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Automatic note detection and response feature completely removed during mediator migration

The automatic note-matching post-processor is deleted (FindNoteRequestHandler at the old backend/ZiziBot.Application/Handlers/Telegram/Note/FindNoteRequest.cs) but no equivalent pipeline is registered in CortexExtension.cs:10-28, so the bot no longer auto-responds to hashtag-triggered notes or text-matching notes.

Impact: Users sending messages containing #note_name or text matching saved notes will no longer receive automatic note responses from the bot.

Migration omission: FindNote post-processor not ported to new pipeline system

In the old MediatR-based pipeline, FindNoteRequestHandler was registered as an IRequestPostProcessor<TRequest, TResponse> via:

.AddOpenRequestPostProcessor(typeof(FindNoteRequestHandler<,>))

in the deleted backend/ZiziBot.Infrastructure/MediatRExtension.cs.

This handler ran after every BotRequestBase handler and would:

  1. Look up all notes for the chat (dataFacade.ChatSetting.GetAllByChat)
  2. Match hashtag words (e.g., #mytag) against note queries
  3. Match the full message text against note queries (case-insensitive)
  4. Send matching notes (text or media) via TelegramService

The new Cortex pipeline in backend/ZiziBot.Application/Extensions/CortexExtension.cs:24-28 registers five ITelegramPostProcessPipeline implementations (CheckAfkSession, EnsureChatAdmin, EnsureChatSetting, UpsertBotUser, InsertChatActivity) but does NOT include any FindNote equivalent. The note CRUD handlers (CreateNoteBotRequest, GetNoteRequest, DeleteNoteRequest) still exist, but the automatic note detection/response pipeline step is entirely missing.

Prompt for agents
The old FindNoteRequestHandler (deleted from backend/ZiziBot.Application/Handlers/Telegram/Note/FindNoteRequest.cs) was a MediatR IRequestPostProcessor that automatically detected notes matching hashtags or message text after every BotRequestBase handler. During migration to Cortex.Mediator, this post-processor was not ported to the new ITelegramPostProcessPipeline system.

To fix: Create a new FindNotePipeline<TRequest, TResponse> class implementing ITelegramPostProcessPipeline<TRequest, TResponse> with the same logic as the deleted FindNoteRequestHandler (checking message text against saved notes, matching hashtags, and sending matching notes). Then register it in CortexExtension.AddApplicationCortexMediator() alongside the other ITelegramPostProcessPipeline registrations, e.g.:
services.AddTransient(typeof(ITelegramPostProcessPipeline<,>), typeof(FindNotePipeline<,>));

The original logic is available in git history at the base commit for reference.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

var htmlMessage = HtmlMessage.Empty
.BoldBr("🎛 ZiziBot Console")
.TextBr("Buka Console untuk mengelola pengaturan, catatan dan lain-lain.")
.TextBr($"OTP: {otp}")

@devin-ai-integration devin-ai-integration Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 One-time password is displayed in group chat messages where it cannot be used

The one-time password is always included in the reply message (TextBr($"OTP: {otp}") at backend/ZiziBot.Application/Features/Handlers/Telegram/Basic/PrepareConsoleRequest.cs:49) regardless of whether the chat is private, so it is visible to all group members even though it is only saved to the database in private chats.

Impact: Users in group chats see a useless OTP that cannot be redeemed, and the code generates an OTP unnecessarily for non-private contexts.

Mechanism: OTP generation moved outside the private-chat guard

In the old code, the OTP was generated inside the if (request.IsPrivateChat) block, so it was never computed or displayed in group chats. In the new code, ValueUtil.GenerateOtp() is called unconditionally at line 21, and the HTML message at line 49 always includes the OTP text. However, the OTP is only persisted to the database inside the if (request.IsPrivateChat) block (lines 23-36), so in group chats the OTP is shown but never stored — making it impossible to redeem.

The OTP line and the database save should both be gated behind the private-chat check, or the OTP should only be appended to the message when request.IsPrivateChat is true.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +23 to +33
try
{
await ApplyActionsAsync(botRequest, actions, htmlMessage);

return PreProcessResult<TResponse>.Stop();
}
catch (Exception exception)
{
logger.LogError(exception, "Error on Action Result pipeline");
return PreProcessResult<TResponse>.Stop();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 ActionResultPipeline now blocks handler execution — behavioral change from old code

The old ActionResultPipelineBehavior (deleted) would apply moderation actions (warn/mute/kick/delete) and then call return await next(cancellationToken), meaning the actual command handler would STILL execute after moderation. The new ActionResultPipeline at backend/ZiziBot.Application/Pipelines/PrePipeline/ActionResultPipeline.cs:27 returns PreProcessResult<TResponse>.Stop(), which completely blocks handler execution.

This means: in the old system, a user who triggered a word filter warning could still have their bot command processed. In the new system, the command is silently dropped. This is a significant behavioral change. If the old pass-through behavior was intentional (e.g., warn but still process the user's request), this change could confuse users whose commands stop working. If the intent was always to block, this is a fix.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +45 to +59
catch (Exception ex)
{
stopwatch.Stop();
logger.LogError(
ex,
"PipelineMetrics Stage={Stage} SessionId={SessionId} RequestType={RequestType} ResponseType={ResponseType} Duration={DurationMs} Status={Status} Message={Message}",
"Request",
sessionId,
requestType,
responseType,
stopwatch.Elapsed.ToString("mm\\:ss\\.fff"),
"Failed",
ex.Message
);
throw;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 LoggingPipeline now re-throws exceptions instead of swallowing them

The old LoggingPipelineBehaviour caught all exceptions and returned new TResponse(), effectively swallowing errors silently. The new LoggingPipeline at backend/ZiziBot.Application/Pipelines/PrePipeline/LoggingPipeline.cs:59 re-throws exceptions after logging. This is a significant behavioral change: previously, any handler exception would result in a silent empty response; now exceptions propagate up to the caller and potentially to the global exception handler middleware. This is generally the correct behavior (errors should not be silently swallowed), but callers that previously relied on the swallowing behavior may now see unhandled exceptions.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 new potential issues.

Open in Devin Review

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Outbox messages are enqueued but no consumer processes them

The new OutboxService (backend/ZiziBot.Application/Services/OutboxService.cs) and OutboxRepository (backend/ZiziBot.Application/Infrastructure/Database/Repository/OutboxRepository.cs) provide infrastructure to enqueue outbox messages, and several handlers now call outboxService.EnqueueAsync(...) (e.g., PostMirrorUserRequest, DeleteMirrorUserRequest, SubmitDonationRequest, WebHookTrakteerDonationRequest). However, there is no background worker, hosted service, or Hangfire job that reads pending outbox messages from the database and processes/dispatches them. The OutboxRepository.GetPendingAsync method exists but is never called. This means all enqueued outbox messages accumulate in MongoDB without being processed.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +39 to +40
if (engineConfig.ConsoleUrl.IsNullOrWhiteSpace())
await serviceFacade.TelegramService.SendMessageText("Maaf fitur ini belum dipersiapkan");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Console setup continues after detecting missing configuration, causing a crash

The handler sends an error message when the console URL is unconfigured (engineConfig.ConsoleUrl.IsNullOrWhiteSpace() at backend/ZiziBot.Application/Features/Handlers/Telegram/Basic/PrepareConsoleRequest.cs:39) but does not return early, so execution falls through to use the null/empty URL on line 43, which will throw or produce a broken link.

Impact: When the console URL is not configured, the bot crashes or sends a broken response instead of gracefully stopping.

Mechanism: missing early return after null-check

At lines 39-40, if ConsoleUrl is null or whitespace, the code sends an error message to the user but does not return. Execution continues to line 43 where engineConfig.ConsoleUrl.SetQueryParam("session_id", sessionId) is called on the null/empty string. Depending on how Flurl handles null inputs, this will either throw a NullReferenceException or produce an invalid URL that gets sent to the user.

The fix is to add return serviceFacade.TelegramService.Complete(); (or similar early return) after line 40.

Prompt for agents
In PrepareConsoleHandler.Handle (PrepareConsoleRequest.cs), after the check at line 39-40 that detects ConsoleUrl is null or whitespace, add an early return so execution does not fall through to line 43 where ConsoleUrl is used. For example, change lines 39-40 to: if (engineConfig.ConsoleUrl.IsNullOrWhiteSpace()) return await serviceFacade.TelegramService.SendMessageText("Maaf fitur ini belum dipersiapkan"); This was also a pre-existing issue in the old code, but the code was reorganized in this PR.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

"ShortCircuited"
);

return result.Response!;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Pre-process pipeline short-circuit may return null response

When a pre-processor calls PreProcessResult<TResponse>.Stop() (the parameterless overload at backend/ZiziBot.Application/Infrastructure/Pipelines/PrePipeline/IPreProcessPipeline.cs:30-33), the Response property is set to default(TResponse), which is null for reference types like BotResponseBase. The pipeline then returns result.Response! at backend/ZiziBot.Application/Infrastructure/Pipelines/PrePipeline/PreProcessPipeline.cs:68 with a null-forgiving operator, which suppresses the compiler warning but will propagate null to the caller.

Currently, FeatureControlPipeline at backend/ZiziBot.Application/Infrastructure/Pipelines/PrePipeline/FeatureControlPipeline.cs:30-35 uses the parameterless Stop() when the response type is not assignable to BotResponseBase. If the response type is a reference type, this will return null to the mediator caller, which may cause a NullReferenceException downstream.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

{
var jobId = $"{CronJobKey.Rss_Prefix}:{rss.Id}";
var rssUrl = await rss.RssUrl.DetectRss(throwIfError: true);
var jobId = await StringUtil.GenerateRssKeyAsync();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 RSS job IDs changed from deterministic to random, preventing deduplication

In the old code, RSS job IDs were deterministic: $"{CronJobKey.Rss_Prefix}:{rss.Id}" (based on the MongoDB entity ID). In the new code at backend/ZiziBot.Application/Features/UseCases/Rss/RegisterRssJobAllUseCase.cs:52 and backend/ZiziBot.Application/Features/Handlers/Telegram/Rss/RegisterRssJobAllRequest.cs:43, job IDs are generated via StringUtil.GenerateRssKeyAsync() which produces a random NanoId. This means:

  1. Each time RSS jobs are re-registered (e.g., on restart via RssRefresh), old Hangfire recurring jobs are removed by HangfireUtil.RemoveRssJobs() but new ones get completely new IDs.
  2. If RegisterRssJobAllUseCase.Handle is called concurrently or multiple times without cleanup, duplicate jobs could be created since the IDs are no longer tied to the entity.
  3. The GenerateRssKeyAsync also calls .ToUpper() on the NanoId, which reduces the entropy of the ID.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

Open in Devin Review

@@ -9,8 +9,8 @@ public class RssSettingEntity : EntityBase
public int? ThreadId { get; set; }
public long UserId { get; set; }
public string RssUrl { get; set; }
public string? OriginalUrl { get; set; }
public string CronJobId { get; set; }
public string? OriginalRssUrl { get; set; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 RSS entity field renamed from OriginalUrl to OriginalRssUrl — existing MongoDB documents may have stale field name

The RssSettingEntity renamed OriginalUrl to OriginalRssUrl at backend/ZiziBot.Application/Infrastructure/Database/MongoDb/Entities/RssSettingEntity.cs:12. A migration (RssRefreshOriginalRssUrlMigration at backend/ZiziBot.Application/Infrastructure/Database/MongoDb/Migrations/RssRefreshOriginalRssUrlMigration.cs) populates the new field from RssUrl where OriginalRssUrl is empty. However, since MongoDB stores field names as-is and the EF Core MongoDB provider maps by property name, existing documents with the old OriginalUrl field will not be read into the new OriginalRssUrl property — they'll appear as null/empty. The migration handles this by copying from RssUrl, but this means the original URL value stored in OriginalUrl is lost. The AddRssUseCase at backend/ZiziBot.Application/Features/UseCases/Rss/AddRssUseCase.cs:43 now correctly sets OriginalRssUrl, and RegisterRssJobAllUseCase at line 48 reads from it.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 68 to 75
rss.Status = EventStatus.Inactive;
rss.LastErrorMessage = e.Message;

logger.LogError("Error registering RSS Job. RssUrl: {RssUrl}, ChatId: {ChatId}, ThreadId: {ThreadId}. Message: {Message}",
rss.RssUrl, rss.ChatId, rss.ThreadId, e.Message);
logger.LogWarning("Error registering RSS Job. RssUrl: {RssUrl}, ChatId: {ChatId}, ThreadId: {ThreadId}. Message: {Message}",
rssUrl, rss.ChatId, rss.ThreadId, e.Message);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 RegisterRssJobAllUseCase uses ParallelForEachAsync with shared DbContext mutations

At backend/ZiziBot.Application/Features/UseCases/Rss/RegisterRssJobAllUseCase.cs:46-74, ParallelForEachAsync is used to process RSS settings concurrently, and each iteration mutates entity properties (rss.RssUrl, rss.CronJobId, rss.Status, etc.) on entities tracked by a shared MongoDbContext. EF Core's DbContext is not thread-safe — concurrent modifications to tracked entities can cause data corruption or exceptions. The SaveChangesAsync() at line 76 then attempts to save all changes. This was present in the new code but not the old handler version at RegisterRssJobAllRequest.cs:41-54 which uses a sequential foreach loop. The use-case version introduces a concurrency risk.

(Refers to lines 46-76)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +41 to +55
var locksToRemove = new List<string>();

foreach (var (key, lockSlim) in FileLocks)
{
if (!lockSlim.IsReadLockHeld && !lockSlim.IsWriteLockHeld && !lockSlim.IsUpgradeableReadLockHeld)
locksToRemove.Add(key);
}

foreach (var key in locksToRemove)
{
if (FileLocks.TryRemove(key, out var lockToDispose))
{
lockToDispose.Dispose();
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 FileUtil cleanup can dispose locks that are concurrently acquired by other threads

In FileUtil.CleanupOldLocksIfNeeded() at backend/ZiziBot.Application/Common/Utils/FileUtil.cs:43-55, the cleanup iterates over FileLocks, identifies locks that appear idle (no read/write/upgradeable locks held), removes them, and disposes them. However, between checking IsReadLockHeld/IsWriteLockHeld at line 45 and calling TryRemove + Dispose at lines 51-53, another thread could call GetFileLockGetOrAdd which returns the existing lock instance before it's removed. That thread would then hold a reference to a lock that gets disposed. Subsequent TryEnterWriteLock/TryEnterReadLock calls on the disposed lock would throw ObjectDisposedException. The window is small but exists under concurrent file operations.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant