PR #6: Bug - Apply DefaultDeadline unconditionally in BuildClientSettings to fix silent timeout bypass#100
Open
AlexanderJohnston wants to merge 40 commits into
Conversation
- Write to the client stream whether or not the checkpoint write succeeds - Do not ignore events routed to stopped flows - Avoid a null reference when an existing flow fails to load
The SkipException handling that required await was removed in a prior commit. Simplify back to returning the task directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Feature/dotnet10 upgrade
Feature/grpc cleanup
Transition core standard libraries to net10.
- Upgrade Totem, Totem.Runtime, Totem.Timeline, Totem.App.Tests from netstandard2.0 to net10.0 - Update Microsoft.Extensions.* package versions from 2.2.0 to 10.0.0 - Replace FormatterServices.GetUninitializedObject() with RuntimeHelpers.GetUninitializedObject() in DurableType.cs - Replace Assembly.LoadWithPartialName() with Assembly.Load(new AssemblyName()) in TypeResolver.cs - Remove ToHashSet extension methods that conflict with built-in LINQ on net10.0 - Fix C# 14 field keyword conflict in Fields.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create new STJ infrastructure replacing Newtonsoft.Json core: - Add TotemJsonTypeInfoResolver: replaces JsonFormatContractResolver and JsonFormatSerializationBinder with DefaultJsonTypeInfoResolver + Modifiers - Many<T> collection creation via expression-compiled factory - Durable type object creation via RuntimeHelpers.GetUninitializedObject() - Property filtering: excludes [Transient], [CompilerGenerated], Notion-declared - Private field/property serialization for durable types - [WriteOnly] attribute support (serialize but don't deserialize) - Add DurableTypeDiscriminatorConverter: JsonConverterFactory handling polymorphic \ property with 'durable:Prefix:TypeName' values for backward compatibility with Newtonsoft-serialized EventStore data. Includes TypeResolver fallback for legacy assembly-qualified type names. - Redesign IJsonFormat: expose JsonSerializerOptions instead of Apply() pattern - Rewrite JsonFormat: simple wrapper around JsonSerializerOptions - Rewrite JsonFormatExtensions: use JsonSerializer/JsonNode instead of JsonConvert/JObject. Add ToJsonNode/ToJsonNodeUtf8 methods replacing JObject. Implement CopyProperties for PopulateObject equivalent. - Rewrite JsonFormatOptions: SerializerOptions replaces SerializerSettings - Rewrite JsonFormatOptionsSetup: configure WriteIndented, CamelCase naming, JsonStringEnumConverter, TotemJsonTypeInfoResolver, DurableTypeDiscriminatorConverter - Update JsonServiceExtensions: use SerializerOptions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… STJ Phase 3 - Timeline Converters: - Rewrite FlowKeyConverter as JsonConverter<FlowKey> using Utf8JsonReader/Writer - Rewrite TimelinePositionConverter as JsonConverter<TimelinePosition> - Update TimelineJsonFormatOptionsSetup to use SerializerOptions Phase 4 - EventStore Layer: - Rewrite SubscribeCommand: replace JObject/JArray/JToken (Newtonsoft.Json.Linq) with JsonNode/JsonArray (System.Text.Json.Nodes) - Use JsonNode.GetValue<T>() instead of JToken.Value<T>() - Use JsonArray instead of JArray, pattern matching with 'is JsonArray' Phase 5 - MVC Layer: - Rewrite WebRuntimeOptionsSetup: replace IPostConfigureOptions<MvcNewtonsoftJsonOptions> with IPostConfigureOptions<JsonOptions>, copying STJ settings instead of 22 Newtonsoft properties - Remove .AddNewtonsoftJson() from ConfigureWebApp.cs (STJ is MVC default) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…iles - Remove Newtonsoft.Json 13.0.3 package from Totem.Runtime.csproj - Remove Microsoft.AspNetCore.Mvc.NewtonsoftJson 10.0.0 package from Totem.Timeline.Mvc.csproj - Delete JsonFormatContractResolver.cs (replaced by TotemJsonTypeInfoResolver) - Delete JsonFormatSerializationBinder.cs (merged into DurableTypeDiscriminatorConverter) All direct Newtonsoft.Json dependencies are now removed. The only remaining Newtonsoft reference is a transitive dependency from EventStore.Client.Grpc (Newtonsoft.Json 9.0.1) which is outside our control. Solution builds with 0 errors across all projects. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The DurableTypeDiscriminatorConverter.Read method was only checking the first property for the \ discriminator. Since JSON property order is not guaranteed, this could miss the discriminator and deserialize as the base type instead of the actual polymorphic type. Now scans through all properties using Utf8JsonReader.TrySkip() to find \ regardless of position in the JSON object. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…o STJ STJ does not auto-detect [TypeConverter] attributes like Newtonsoft did. This caused deserialization crashes for 20+ types (TypeName, Id, FileLink, etc.) that use TextConverter for string-based serialization. The factory detects types with a non-default TypeConverter that supports string conversion, and serializes/deserializes them as JSON strings via ConvertFromInvariantString/ConvertToInvariantString. Registered in JsonFormatOptionsSetup.Configure() after JsonStringEnumConverter, before the durable type converters in PostConfigure(). No conflicts since TypeConverter types are not durable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ctory Add TypeConverterJsonConverterFactory to bridge [TypeConverter] types to STJ
…only The previous check matched all .NET built-in types with TypeConverters (int, DateTime, bool, Guid, etc.), causing reader.GetString() to throw on non-string JSON tokens. This silently broke ASP.NET model binding for any model with non-string properties (e.g. SmartScanRecord.FileCount). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Narrow TypeConverterJsonConverterFactory and add dictionary key support
Refactor/newtonsoft to stj
When TryStart() encounters FlowInfo.Stopped, complete the lifetime directly instead of throwing through the ObserveNextPoint catch path, which redundantly re-enters Stop() and writes a new checkpoint per queued event. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The ConnectionString early-return path was missing the DefaultDeadline assignment, so using a connection string bypassed the configured timeout entirely. Restructure to converge both paths before applying shared settings (LoggerFactory, DefaultDeadline). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DefaultDeadline unconditionally in BuildClientSettings to fix silent timeout bypassDefaultDeadline unconditionally in BuildClientSettings to fix silent timeout bypass
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When ESDB experiences transient I/O pressure (chunk caching/uncaching, storage writer queue backup), gRPC calls from Totem can exceed the configured
DefaultDeadlineand returnDeadlineExceeded. This permanently kills the affected flow — there is no retry, no backoff, and no recovery without manual intervention.BuildClientSettingsinEventStoreServiceExtensionshas two paths depending on whetheroptions.ConnectionStringis set:When the
ConnectionStringearly-return path is taken,DefaultDeadlineis never assigned. The gRPC client is created with no per-call deadline, falling back to whatever the EventStore .NET client's internal default happens to be (no deadline by default in gRPC — calls can wait indefinitely, or the client may impose its own).The
ConnectionStringproperty defaults tonull, but if any configuration source populates it — an environment variable, a Docker/Kubernetes secret, a hosting platform injectingConnectionStrings__EventStore, or any other source in Microsoft's configuration merge order (JSON → env vars → command line) — the structured options inappsettings.jsonare silently bypassed. The operator's 15-minute timeout configuration has no effect.Even if the
ConnectionStringpath is not the active cause of the 10s deadline in this specific incident, the bug is real: any operator who switches to connection-string-based configuration will silently lose all timeout protection.LoggerFactoryhad the same issue — it was set redundantly in both paths, butDefaultDeadlinewas only set in one.Fix
Restructure
BuildClientSettingsso both paths converge before applying shared settings:LoggerFactoryandDefaultDeadlineare now assigned exactly once, unconditionally, after both paths produce anEventStoreClientSettingsinstance.What this preserves
DefaultDeadlineset fromoptions.Connection.TimeoutDefaultDeadlinenot set — silent omissionoptions.Connection.TimeoutLoggerFactoryassignmentEventStoreClientSettings.Create(connStr)elseblockWhat this changes
DefaultDeadline. Previously it had none. If the connection string itself contains a timeout parameter,DefaultDeadlinesets the per-call default but gRPC per-call deadlines explicitly passed to individual operations still take precedence. No conflict.Scope
One method in one file. No new packages, no behavioural changes to the structured path, no changes to flow runtime logic.