Skip to content

Fix DateTime timezone mismatch causing Delay step to hang indefinitely with PostgreSQL#1425

Open
Copilot wants to merge 4 commits intomasterfrom
copilot/fix-delay-step-timezone-mismatch
Open

Fix DateTime timezone mismatch causing Delay step to hang indefinitely with PostgreSQL#1425
Copilot wants to merge 4 commits intomasterfrom
copilot/fix-delay-step-timezone-mismatch

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 10, 2026

When using PostgreSQL persistence (timestamp with time zone columns) in non-UTC environments, EF Core returns DateTime values with Kind=Local. The prior code used DateTime.SpecifyKind(..., DateTimeKind.Utc) which relabels the kind without converting the value, introducing a UTC offset into all persisted timestamps — causing SleepUntil to be read as e.g. 1 hour later than stored, and the Delay step to never resume.

Describe your implementation or design

Added a private EnsureUtc helper in ExtensionMethods.cs that performs a proper conversion:

private static DateTime EnsureUtc(DateTime dateTime)
{
    if (dateTime.Kind == DateTimeKind.Local)
        return dateTime.ToUniversalTime();
    if (dateTime.Kind == DateTimeKind.Utc)
        return dateTime;
    return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
}

The helper handles all three DateTimeKind cases: Local values are properly converted to UTC via ToUniversalTime(), Utc values are returned as-is (avoiding an unnecessary copy), and Unspecified values are labelled as UTC via SpecifyKind (preserving the previous behaviour for databases like SQLite and SQL Server).

Applied EnsureUtc to all DateTime fields read from the persistence layer:

  • ToWorkflowInstance: CreateTime, CompleteTime, SleepUntil, StartTime, EndTime
  • ToEventSubscription: SubscribeAsOf
  • ToEvent: EventTime

ToPersistable write paths are unchanged — values are already UTC when written.

Tests

Added [assembly: InternalsVisibleTo("WorkflowCore.Tests.Sqlite")] to the EF persistence assembly and added ExtensionMethodsFixture in WorkflowCore.Tests.Sqlite with 4 unit tests that directly call ToWorkflowInstance() with all three DateTimeKind variants (Local, Utc, Unspecified) for CreateTime and SleepUntil. The Kind=Local tests compute the expected UTC value using .ToUniversalTime() at test time, making them timezone-agnostic and runnable in any CI environment. All 10 SQLite persistence tests pass (6 existing + 4 new).

Breaking change

No. Behavior is corrected for non-UTC environments; UTC environments see no change.

Additional context

The DateTimeKind.Unspecified case (e.g., SQLite, SQL Server without timestamp with time zone) continues to behave exactly as before via the SpecifyKind fallback.

…tuck with PostgreSQL

Agent-Logs-Url: https://github.com/danielgerlag/workflow-core/sessions/85da3a83-cc5a-4865-8002-bac682193a86

Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix delay step getting stuck due to timezone mismatch Fix DateTime timezone mismatch causing Delay step to hang indefinitely with PostgreSQL Apr 10, 2026
Copilot AI requested a review from danielgerlag April 10, 2026 14:27
@danielgerlag danielgerlag requested a review from Copilot April 10, 2026 22:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes an Entity Framework (EF Core) persistence timezone mismatch where timestamp with time zone values from PostgreSQL can be materialized as DateTimeKind.Local, causing SleepUntil/Delay-related timestamps to be interpreted incorrectly and workflows to hang.

Changes:

  • Added an EnsureUtc helper to properly convert DateTimeKind.Local values to UTC via ToUniversalTime.
  • Replaced DateTime.SpecifyKind(..., Utc) with EnsureUtc(...) for DateTime fields read from EF persistence models (WorkflowInstance, EventSubscription, Event).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +140 to +145
private static DateTime EnsureUtc(DateTime dateTime)
{
if (dateTime.Kind == DateTimeKind.Local)
return dateTime.ToUniversalTime();
return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

The new EnsureUtc behavior (converting DateTimeKind.Local via ToUniversalTime) isn’t covered by an automated regression test, so it could regress back to SpecifyKind without being detected—especially since most CI environments run with TimeZoneInfo.Local set to UTC. Consider adding a test (e.g., in the EF/PostgreSQL persistence tests) that forces a non-UTC local zone for the process (on Linux, setting TZ + calling TimeZoneInfo.ClearCachedData()), then verifies that a persisted SleepUntil coming back as Kind=Local is converted to the expected UTC instant.

Copilot uses AI. Check for mistakes.
private static DateTime EnsureUtc(DateTime dateTime)
{
if (dateTime.Kind == DateTimeKind.Local)
return dateTime.ToUniversalTime();
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

EnsureUtc currently calls DateTime.SpecifyKind(dateTime, DateTimeKind.Utc) for inputs that are already DateTimeKind.Utc. Returning the original value when dateTime.Kind == DateTimeKind.Utc would make the intent clearer and avoid an unnecessary copy.

Suggested change
return dateTime.ToUniversalTime();
return dateTime.ToUniversalTime();
if (dateTime.Kind == DateTimeKind.Utc)
return dateTime;

Copilot uses AI. Check for mistakes.
@danielgerlag
Copy link
Copy Markdown
Owner

@copilot please address feedback

…Local/Utc/Unspecified DateTime conversion

Agent-Logs-Url: https://github.com/danielgerlag/workflow-core/sessions/ffb23322-90eb-473d-a351-30db6c1ef619

Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 10, 2026

@copilot please address feedback

Done in commit 6da36df:

  1. EnsureUtc optimization: Added an early return for DateTimeKind.Utc to avoid the unnecessary SpecifyKind call.

  2. Regression tests: Added InternalsVisibleTo("WorkflowCore.Tests.Sqlite") to the EF persistence assembly and added ExtensionMethodsFixture with 4 unit tests that directly call ToWorkflowInstance() with Kind=Local, Kind=Utc, and Kind=Unspecified datetimes. The Kind=Local tests compute the expected UTC value using .ToUniversalTime() at test time, making them timezone-agnostic while still verifying correct conversion behaviour.

The ExternalTokenExpiry DateTime? field was missed in the initial fix and
would still suffer from incorrect UTC handling when PostgreSQL returns
DateTime values with Kind=Local.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielgerlag danielgerlag marked this pull request as ready for review April 20, 2026 00:44
@danielgerlag danielgerlag requested a review from glucaci as a code owner April 20, 2026 00:44
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.

Delay step stuck indefinitely due to timezone mismatch with PostgreSQL persistence

3 participants