Skip to content

ElasticSerializer async members run synchronous stream I/O and ignore the cancellation token #580

Description

@alexeyzimarev

src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticSerializer.cs

All four async members of ElasticSerializer delegate straight to their synchronous counterparts and hand back an already-completed task:

public Task<object> DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = default)
    => Task.FromResult(Deserialize(type, stream));

public Task<T> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default)
    => Task.FromResult(Deserialize<T>(stream));

public Task SerializeAsync<T>(T data, Stream stream, SerializationFormatting formatting = SerializationFormatting.None, CancellationToken cancellationToken = default) {
    Serialize(data, stream, formatting);

    return Task.CompletedTask;
}

Two consequences:

  1. The stream I/O blocks the calling thread. Deserialize reads the response stream synchronously and Serialize writes through a Utf8JsonWriter synchronously. A caller that awaits DeserializeAsync still pays a blocking read, so the async path buys nothing and can starve the thread pool under load.
  2. cancellationToken is accepted and silently discarded on all four members. Callers reasonably expect a long read to be cancellable; it isn't.

Suggested fix

  • JsonSerializer.DeserializeAsync(stream, type, _options, cancellationToken) for the deserialize path.
  • JsonSerializer.SerializeAsync(stream, data, _options, cancellationToken) for the serialize path — or keep Utf8JsonWriter and switch to FlushAsync + DisposeAsync.
  • Route the non-PersistedEvent branch of SerializeAsync through builtIn.SerializeAsync rather than builtIn.Serialize.

The synchronous members must stay synchronous — IElasticsearchSerializer declares them that way, so they can't be changed. This issue is only about the async ones.

Blocker

ElasticSerializer has no test coveragesrc/Experimental/test/ contains only the Spyglass projects. Round-trip tests should land first (or alongside), covering at minimum a PersistedEvent whose Message is remapped through ITypeMapper, since that nested re-deserialization is the part most likely to break in a rewrite.

Context

Surfaced by the Qodo review bot on #578, which flagged the synchronous I/O on lines that are sync by interface contract and were equally synchronous before that PR. Declined there as out of scope for a disposal fix; the real problem is the async delegation above, which predates it.

Low priority — the package is under src/Experimental/.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestperfornamceWays to improve performance

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions