From 5bd189a5554a981f4284698c5db7b73db366eb35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bragan=C3=A7a?= <495495+thefringeninja@users.noreply.github.com> Date: Wed, 28 Feb 2024 01:36:29 -0800 Subject: [PATCH] Subscriptions Without Callbacks (DEV-112) (#282) * add sensible subscriptions * add sensible persistent subscriptions * obsolete old subscription / ps apis * put missing props on sever exception in net48 --- Directory.Build.props | 4 +- samples/persistent-subscriptions/Program.cs | 139 ++-- samples/server-side-filtering/Program.cs | 178 +++-- samples/subscribing-to-streams/Program.cs | 288 ++++--- .../AsyncStreamReaderExtensions.cs | 16 +- .../protos/streams.proto | 13 +- ...StorePersistentSubscriptionsClient.Read.cs | 361 ++++++++- ...EventStorePersistentSubscriptionsClient.cs | 10 +- .../PersistentSubscription.cs | 188 ++--- .../PersistentSubscriptionMessage.cs | 33 + .../EventStore.Client.Streams.csproj | 3 - .../EventStoreClient.Append.cs | 15 +- .../EventStoreClient.Read.cs | 71 +- .../EventStoreClient.Subscriptions.cs | 233 +++++- .../EventStoreClient.cs | 12 +- .../StreamMessage.cs | 32 + .../StreamSubscription.cs | 160 ++-- .../Streams/BatchAppendReq.cs | 4 +- .../SubscriptionConfirmation.cs | 24 - .../SubscriptionFilterOptions.cs | 24 +- .../EventStore.Client.csproj | 6 +- .../Bugs/Issue_1125.cs | 77 +- ...connect_to_existing_with_max_one_client.cs | 35 +- ...o_existing_with_max_one_client_obsolete.cs | 43 ++ .../connect_to_existing_with_permissions.cs | 31 +- ...t_to_existing_with_permissions_obsolete.cs | 37 + ...t_to_existing_with_start_from_beginning.cs | 75 +- ...ting_with_start_from_beginning_obsolete.cs | 66 ++ ...ect_to_existing_with_start_from_not_set.cs | 76 +- ...isting_with_start_from_not_set_obsolete.cs | 64 ++ ...h_start_from_not_set_then_event_written.cs | 73 +- ...rom_not_set_then_event_written_obsolete.cs | 72 ++ ...ing_with_start_from_set_to_end_position.cs | 76 +- ...start_from_set_to_end_position_obsolete.cs | 63 ++ ..._set_to_end_position_then_event_written.cs | 72 +- ...nd_position_then_event_written_obsolete.cs | 69 ++ ...art_from_set_to_invalid_middle_position.cs | 58 +- ...set_to_invalid_middle_position_obsolete.cs | 49 ++ ...start_from_set_to_valid_middle_position.cs | 63 +- ...m_set_to_valid_middle_position_obsolete.cs | 65 ++ ...connect_to_existing_without_permissions.cs | 22 +- ...o_existing_without_permissions_obsolete.cs | 32 + ...o_existing_without_read_all_permissions.cs | 23 +- ...g_without_read_all_permissions_obsolete.cs | 33 + ...onnect_to_non_existing_with_permissions.cs | 23 +- ..._non_existing_with_permissions_obsolete.cs | 32 + .../SubscriptionToAll/connect_with_retries.cs | 75 +- .../connect_with_retries_obsolete.cs | 59 ++ .../deleting_existing_with_subscriber.cs | 66 +- ...eting_existing_with_subscriber_obsolete.cs | 66 ++ .../SubscriptionToAll/get_info.cs | 109 ++- .../SubscriptionToAll/get_info_obsolete.cs | 183 +++++ ...atching_up_to_link_to_events_manual_ack.cs | 78 +- ...p_to_link_to_events_manual_ack_obsolete.cs | 78 ++ ...catching_up_to_normal_events_manual_ack.cs | 67 +- ...up_to_normal_events_manual_ack_obsolete.cs | 65 ++ .../SubscriptionToAll/happy_case_filtered.cs | 65 +- .../happy_case_filtered_obsolete.cs | 72 ++ ...happy_case_filtered_with_start_from_set.cs | 75 +- ...e_filtered_with_start_from_set_obsolete.cs | 86 +++ ...subscribing_to_normal_events_manual_ack.cs | 69 +- ...ng_to_normal_events_manual_ack_obsolete.cs | 65 ++ .../update_existing_filtered_obsolete.cs | 30 + .../update_existing_with_check_point.cs | 178 ++--- ...date_existing_with_check_point_filtered.cs | 175 ++--- ...ting_with_check_point_filtered_obsolete.cs | 120 +++ .../update_existing_with_subscribers.cs | 66 +- ...date_existing_with_subscribers_obsolete.cs | 60 ++ .../when_writing_and_filtering_out_events.cs | 152 ++-- ...iting_and_filtering_out_events_obsolete.cs | 108 +++ ...ubscribing_to_normal_events_manual_nack.cs | 72 +- ...g_to_normal_events_manual_nack_obsolete.cs | 68 ++ ...connect_to_existing_with_max_one_client.cs | 41 +- ...o_existing_with_max_one_client_obsolete.cs | 47 ++ .../connect_to_existing_with_permissions.cs | 28 +- ...t_to_existing_with_permissions_obsolete.cs | 39 + ...h_start_from_beginning_and_events_in_it.cs | 61 +- ...rom_beginning_and_events_in_it_obsolete.cs | 65 ++ ...with_start_from_beginning_and_no_stream.cs | 55 +- ...t_from_beginning_and_no_stream_obsolete.cs | 65 ++ ...ith_start_from_not_set_and_events_in_it.cs | 56 +- ..._from_not_set_and_events_in_it_obsolete.cs | 61 ++ ...set_and_events_in_it_then_event_written.cs | 52 +- ...vents_in_it_then_event_written_obsolete.cs | 67 ++ ...om_set_to_end_position_and_events_in_it.cs | 54 +- ..._end_position_and_events_in_it_obsolete.cs | 60 ++ ...ion_and_events_in_it_then_event_written.cs | 64 +- ...vents_in_it_then_event_written_obsolete.cs | 72 ++ ...sting_with_start_from_two_and_no_stream.cs | 51 +- ...h_start_from_two_and_no_stream_obsolete.cs | 65 ++ ..._with_start_from_x_set_and_events_in_it.cs | 53 +- ...rt_from_x_set_and_events_in_it_obsolete.cs | 65 ++ ...set_and_events_in_it_then_event_written.cs | 61 +- ...vents_in_it_then_event_written_obsolete.cs | 68 ++ ...n_x_and_events_in_it_then_event_written.cs | 63 +- ...vents_in_it_then_event_written_obsolete.cs | 72 ++ ...connect_to_existing_without_permissions.cs | 19 +- ...o_existing_without_permissions_obsolete.cs | 35 + ...onnect_to_non_existing_with_permissions.cs | 29 +- ..._non_existing_with_permissions_obsolete.cs | 34 + .../connect_with_retries.cs | 78 +- .../connect_with_retries_obsolete.cs | 70 ++ ...connecting_to_a_persistent_subscription.cs | 72 +- ...g_to_a_persistent_subscription_obsolete.cs | 72 ++ .../deleting_existing_with_subscriber.cs | 64 +- ...eting_existing_with_subscriber_obsolete.cs | 67 ++ .../SubscriptionToStream/get_info.cs | 92 +-- .../SubscriptionToStream/get_info_obsolete.cs | 199 +++++ ...atching_up_to_link_to_events_manual_ack.cs | 84 +-- ...p_to_link_to_events_manual_ack_obsolete.cs | 80 ++ ...catching_up_to_normal_events_manual_ack.cs | 71 +- ...up_to_normal_events_manual_ack_obsolete.cs | 67 ++ ...subscribing_to_normal_events_manual_ack.cs | 66 +- ...ng_to_normal_events_manual_ack_obsolete.cs | 67 ++ .../update_existing_with_check_point.cs | 167 ++--- ...date_existing_with_check_point_obsolete.cs | 124 +++ .../update_existing_with_subscribers.cs | 70 +- ...date_existing_with_subscribers_obsolete.cs | 61 ++ ...ubscribing_to_normal_events_manual_nack.cs | 73 +- ...g_to_normal_events_manual_nack_obsolete.cs | 70 ++ .../Bugs/Issue_104.cs | 3 +- .../Bugs/Issue_2544.cs | 3 +- .../EventStore.Client.Streams.Tests.csproj | 6 +- .../Security/SecurityFixture.cs | 16 +- .../all_stream_with_no_acl_security.cs | 6 +- .../overriden_system_stream_security.cs | 6 +- ...verriden_system_stream_security_for_all.cs | 5 +- .../overriden_user_stream_security.cs | 6 +- .../Security/subscribe_to_all_security.cs | 10 +- .../Security/subscribe_to_stream_security.cs | 27 +- .../subscribe_to_stream_security_obsolete.cs | 75 ++ .../Security/system_stream_security.cs | 12 +- .../Subscriptions/reconnection.cs | 4 +- .../Subscriptions/subscribe_to_all.cs | 704 +++++++----------- .../subscribe_to_all_obsolete.cs | 592 +++++++++++++++ .../Subscriptions/subscribe_to_stream.cs | 290 +++----- .../subscribe_to_stream_obsolete.cs | 303 ++++++++ .../Extensions/TaskExtensions.cs | 2 +- 138 files changed, 7364 insertions(+), 3372 deletions(-) create mode 100644 src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionMessage.cs delete mode 100644 src/EventStore.Client.Streams/SubscriptionConfirmation.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_filtered_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers_obsolete.cs create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs create mode 100644 test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security_obsolete.cs create mode 100644 test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all_obsolete.cs create mode 100644 test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream_obsolete.cs diff --git a/Directory.Build.props b/Directory.Build.props index 5c261f63f..1d320aee3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,8 +13,8 @@ pdbonly true - 2.59.0 - 2.59.0 + 2.60.0 + 2.60.0 diff --git a/samples/persistent-subscriptions/Program.cs b/samples/persistent-subscriptions/Program.cs index 6ea61fa67..e1e9387c6 100644 --- a/samples/persistent-subscriptions/Program.cs +++ b/samples/persistent-subscriptions/Program.cs @@ -7,10 +7,21 @@ await CreatePersistentSubscription(client); await UpdatePersistentSubscription(client); -await ConnectToPersistentSubscriptionToStream(client); + +try { + await ConnectToPersistentSubscriptionToStream(client, GetCT()); +} catch (OperationCanceledException) { } + await CreatePersistentSubscriptionToAll(client); -await ConnectToPersistentSubscriptionToAll(client); -await ConnectToPersistentSubscriptionWithManualAcks(client); + +try { + await ConnectToPersistentSubscriptionToAll(client, GetCT()); +} catch (OperationCanceledException) { } + +try { + await ConnectToPersistentSubscriptionWithManualAcks(client, GetCT()); +} catch (OperationCanceledException) { } + await GetPersistentSubscriptionToStreamInfo(client); await GetPersistentSubscriptionToAllInfo(client); await ReplayParkedToStream(client); @@ -39,25 +50,30 @@ await client.CreateToStreamAsync( ); Console.WriteLine("Subscription to stream created"); + #endregion create-persistent-subscription-to-stream } -static async Task ConnectToPersistentSubscriptionToStream(EventStorePersistentSubscriptionsClient client) { +static async Task ConnectToPersistentSubscriptionToStream(EventStorePersistentSubscriptionsClient client, + CancellationToken ct) { #region subscribe-to-persistent-subscription-to-stream - var subscription = await client.SubscribeToStreamAsync( + await using var subscription = client.SubscribeToStream( "test-stream", - "subscription-group", - async (subscription, evnt, retryCount, cancellationToken) => { - await HandleEvent(evnt); - await subscription.Ack(evnt); - }, - (subscription, dropReason, exception) => { - Console.WriteLine($"Subscription to stream was dropped due to {dropReason}. {exception?.Message}"); + "subscription-group", + cancellationToken: ct); + await foreach (var message in subscription.Messages) { + switch (message) { + case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): + Console.WriteLine($"Subscription {subscriptionId} to stream started"); + break; + case PersistentSubscriptionMessage.Event(var resolvedEvent, _): + await HandleEvent(resolvedEvent); + await subscription.Ack(resolvedEvent); + break; } - ); + } - Console.WriteLine("Subscription to stream started"); #endregion subscribe-to-persistent-subscription-to-stream } @@ -65,7 +81,7 @@ static async Task CreatePersistentSubscriptionToAll(EventStorePersistentSubscrip #region create-persistent-subscription-to-all var userCredentials = new UserCredentials("admin", "changeit"); - var filter = StreamFilter.Prefix("test"); + var filter = StreamFilter.Prefix("test"); var settings = new PersistentSubscriptionSettings(); await client.CreateToAllAsync( @@ -76,45 +92,56 @@ await client.CreateToAllAsync( ); Console.WriteLine("Subscription to all created"); + #endregion create-persistent-subscription-to-all } -static async Task ConnectToPersistentSubscriptionToAll(EventStorePersistentSubscriptionsClient client) { +static async Task ConnectToPersistentSubscriptionToAll(EventStorePersistentSubscriptionsClient client, + CancellationToken ct) { #region subscribe-to-persistent-subscription-to-all - await client.SubscribeToAllAsync( + await using var subscription = client.SubscribeToAll( "subscription-group", - async (subscription, evnt, retryCount, cancellationToken) => { await HandleEvent(evnt); }, - (subscription, dropReason, exception) => { - Console.WriteLine($"Subscription to all was dropped due to {dropReason}. {exception?.Message}"); + cancellationToken: ct); + + await foreach (var message in subscription.Messages) { + switch (message) { + case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): + Console.WriteLine($"Subscription {subscriptionId} to stream started"); + break; + case PersistentSubscriptionMessage.Event(var resolvedEvent, _): + await HandleEvent(resolvedEvent); + break; } - ); + } - Console.WriteLine("Subscription to all started"); #endregion subscribe-to-persistent-subscription-to-all } -static async Task ConnectToPersistentSubscriptionWithManualAcks(EventStorePersistentSubscriptionsClient client) { +static async Task ConnectToPersistentSubscriptionWithManualAcks(EventStorePersistentSubscriptionsClient client, + CancellationToken ct) { #region subscribe-to-persistent-subscription-with-manual-acks - var subscription = await client.SubscribeToStreamAsync( + await using var subscription = client.SubscribeToStream( "test-stream", "subscription-group", - async (subscription, evnt, retryCount, cancellationToken) => { - try { - await HandleEvent(evnt); - await subscription.Ack(evnt); - } - catch (UnrecoverableException ex) { - await subscription.Nack(PersistentSubscriptionNakEventAction.Park, ex.Message, evnt); - } - }, - (subscription, dropReason, exception) => { - Console.WriteLine($"Subscription to stream with manual acks was dropped due to {dropReason}. {exception?.Message}"); + cancellationToken: ct); + await foreach (var message in subscription.Messages) { + switch (message) { + case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): + Console.WriteLine($"Subscription {subscriptionId} to stream with manual acks started"); + break; + case PersistentSubscriptionMessage.Event(var resolvedEvent, _): + try { + await HandleEvent(resolvedEvent); + await subscription.Ack(resolvedEvent); + } catch (UnrecoverableException ex) { + await subscription.Nack(PersistentSubscriptionNakEventAction.Park, ex.Message, resolvedEvent); + } + break; } - ); + } - Console.WriteLine("Subscription to stream with manual acks started"); #endregion subscribe-to-persistent-subscription-with-manual-acks } @@ -132,6 +159,7 @@ await client.UpdateToStreamAsync( ); Console.WriteLine("Subscription updated"); + #endregion update-persistent-subscription } @@ -147,14 +175,12 @@ await client.DeleteToStreamAsync( ); Console.WriteLine("Subscription to stream deleted"); - } - catch (PersistentSubscriptionNotFoundException) { + } catch (PersistentSubscriptionNotFoundException) { // ignore - } - catch (Exception ex) { + } catch (Exception ex) { Console.WriteLine($"Subscription to stream delete error: {ex.GetType()} {ex.Message}"); } - + #endregion delete-persistent-subscription } @@ -169,11 +195,9 @@ await client.DeleteToAllAsync( ); Console.WriteLine("Subscription to all deleted"); - } - catch (PersistentSubscriptionNotFoundException) { + } catch (PersistentSubscriptionNotFoundException) { // ignore - } - catch (Exception ex) { + } catch (Exception ex) { Console.WriteLine($"Subscription to all delete error: {ex.GetType()} {ex.Message}"); } @@ -221,6 +245,7 @@ await client.ReplayParkedMessagesToStreamAsync( ); Console.WriteLine("Replay of parked messages to stream requested"); + #endregion persistent-subscription-replay-parked-to-stream } @@ -235,6 +260,7 @@ await client.ReplayParkedMessagesToAllAsync( ); Console.WriteLine("Replay of parked messages to all requested"); + #endregion replay-parked-of-persistent-subscription-to-all } @@ -249,7 +275,7 @@ static async Task ListPersistentSubscriptionsToStream(EventStorePersistentSubscr var entries = subscriptions .Select(s => $"GroupName: {s.GroupName} EventSource: {s.EventSource} Status: {s.Status}"); - + Console.WriteLine($"Subscriptions to stream: [ {string.Join(", ", entries)} ]"); #endregion list-persistent-subscriptions-to-stream @@ -259,13 +285,13 @@ static async Task ListPersistentSubscriptionsToAll(EventStorePersistentSubscript #region list-persistent-subscriptions-to-all var userCredentials = new UserCredentials("admin", "changeit"); - var subscriptions = await client.ListToAllAsync(userCredentials: userCredentials); + var subscriptions = await client.ListToAllAsync(userCredentials: userCredentials); var entries = subscriptions .Select(s => $"GroupName: {s.GroupName} EventSource: {s.EventSource} Status: {s.Status}"); - + Console.WriteLine($"Subscriptions to all: [ {string.Join(", ", entries)} ]"); - + #endregion list-persistent-subscriptions-to-all } @@ -273,13 +299,13 @@ static async Task ListAllPersistentSubscriptions(EventStorePersistentSubscriptio #region list-persistent-subscriptions var userCredentials = new UserCredentials("admin", "changeit"); - var subscriptions = await client.ListAllAsync(userCredentials: userCredentials); + var subscriptions = await client.ListAllAsync(userCredentials: userCredentials); var entries = subscriptions .Select(s => $"GroupName: {s.GroupName} EventSource: {s.EventSource} Status: {s.Status}"); - + Console.WriteLine($"Subscriptions: [{string.Join(", ", entries)} ]"); - + #endregion list-persistent-subscriptions } @@ -290,9 +316,14 @@ static async Task RestartPersistentSubscriptionSubsystem(EventStorePersistentSub await client.RestartSubsystemAsync(userCredentials: userCredentials); Console.WriteLine("Persistent subscription subsystem restarted"); + #endregion restart-persistent-subscription-subsystem } static Task HandleEvent(ResolvedEvent evnt) => Task.CompletedTask; -class UnrecoverableException : Exception { } \ No newline at end of file +// ensures that samples exit in a timely manner on CI +static CancellationToken GetCT() => new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token; + +class UnrecoverableException : Exception { +} diff --git a/samples/server-side-filtering/Program.cs b/samples/server-side-filtering/Program.cs index 243e40b60..b9e5de8c8 100644 --- a/samples/server-side-filtering/Program.cs +++ b/samples/server-side-filtering/Program.cs @@ -8,22 +8,23 @@ await using var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://localhost:2113?tls=false")); -await client.SubscribeToAllAsync( - FromAll.Start, - (s, e, c) => { - Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); - semaphore.Release(); - return Task.CompletedTask; - }, - filterOptions: new SubscriptionFilterOptions( - EventTypeFilter.Prefix("some-"), - 1, - (s, p, c) => { - Console.WriteLine($"checkpoint taken at {p.PreparePosition}"); - return Task.CompletedTask; +_ = Task.Run(async () => { + await using var subscription = client.SubscribeToAll( + FromAll.Start, + filterOptions: new SubscriptionFilterOptions(EventTypeFilter.Prefix("some-"))); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var e): + Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); + semaphore.Release(); + break; + case StreamMessage.AllStreamCheckpointReached(var p): + Console.WriteLine($"checkpoint taken at {p.PreparePosition}"); + break; } - ) -); + } +}); + await Task.Delay(2000); @@ -48,14 +49,16 @@ await client.AppendToStreamAsync( static async Task ExcludeSystemEvents(EventStoreClient client) { #region exclude-system - await client.SubscribeToAllAsync( + await using var subscription = client.SubscribeToAll( FromAll.Start, - (s, e, c) => { - Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); - return Task.CompletedTask; - }, - filterOptions: new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()) - ); + filterOptions: new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents())); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var e): + Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); + break; + } + } #endregion exclude-system } @@ -63,114 +66,107 @@ await client.SubscribeToAllAsync( static async Task EventTypePrefix(EventStoreClient client) { #region event-type-prefix - var filter = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-")); + var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-")); #endregion event-type-prefix - await client.SubscribeToAllAsync( - FromAll.Start, - (s, e, c) => { - Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); - return Task.CompletedTask; - }, - filterOptions: filter - ); + await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var e): + Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); + break; + } + } } static async Task EventTypeRegex(EventStoreClient client) { #region event-type-regex - var filter = new SubscriptionFilterOptions(EventTypeFilter.RegularExpression("^user|^company")); + var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.RegularExpression("^user|^company")); #endregion event-type-regex - await client.SubscribeToAllAsync( - FromAll.Start, - (s, e, c) => { - Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); - return Task.CompletedTask; - }, - filterOptions: filter - ); + await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var e): + Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); + break; + } + } } static async Task StreamPrefix(EventStoreClient client) { #region stream-prefix - var filter = new SubscriptionFilterOptions(StreamFilter.Prefix("user-")); + var filterOptions = new SubscriptionFilterOptions(StreamFilter.Prefix("user-")); #endregion stream-prefix - await client.SubscribeToAllAsync( - FromAll.Start, - (s, e, c) => { - Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); - return Task.CompletedTask; - }, - filterOptions: filter - ); + await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var e): + Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); + break; + } + } } static async Task StreamRegex(EventStoreClient client) { #region stream-regex - var filter = new SubscriptionFilterOptions(StreamFilter.RegularExpression("^account|^savings")); + var filterOptions = new SubscriptionFilterOptions(StreamFilter.RegularExpression("^account|^savings")); #endregion stream-regex - await client.SubscribeToAllAsync( - FromAll.Start, - (s, e, c) => { - Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); - return Task.CompletedTask; - }, - filterOptions: filter - ); + await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var e): + Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); + break; + } + } } static async Task CheckpointCallback(EventStoreClient client) { #region checkpoint - var filter = new SubscriptionFilterOptions( - EventTypeFilter.ExcludeSystemEvents(), - checkpointReached: (s, p, c) => { - Console.WriteLine($"checkpoint taken at {p.PreparePosition}"); - return Task.CompletedTask; + var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()); + + await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var e): + Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); + break; + case StreamMessage.AllStreamCheckpointReached(var p): + Console.WriteLine($"checkpoint taken at {p.PreparePosition}"); + break; } - ); - + } + #endregion checkpoint - - await client.SubscribeToAllAsync( - FromAll.Start, - (s, e, c) => { - Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); - return Task.CompletedTask; - }, - filterOptions: filter - ); } static async Task CheckpointCallbackWithInterval(EventStoreClient client) { #region checkpoint-with-interval - var filter = new SubscriptionFilterOptions( - EventTypeFilter.ExcludeSystemEvents(), - 1000, - (s, p, c) => { - Console.WriteLine($"checkpoint taken at {p.PreparePosition}"); - return Task.CompletedTask; - } - ); + var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents(), 1000); #endregion checkpoint-with-interval - await client.SubscribeToAllAsync( - FromAll.Start, - (s, e, c) => { - Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); - return Task.CompletedTask; - }, - filterOptions: filter - ); -} \ No newline at end of file + await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var e): + Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}"); + break; + case StreamMessage.AllStreamCheckpointReached(var p): + Console.WriteLine($"checkpoint taken at {p.PreparePosition}"); + break; + } + } +} diff --git a/samples/subscribing-to-streams/Program.cs b/samples/subscribing-to-streams/Program.cs index b0a59ea22..90fa29737 100644 --- a/samples/subscribing-to-streams/Program.cs +++ b/samples/subscribing-to-streams/Program.cs @@ -5,91 +5,159 @@ await using var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://localhost:2113?tls=false")); -await SubscribeToStream(client); -await SubscribeToAll(client); -await OverridingUserCredentials(client); +await Task.WhenAll(YieldSamples().Select(async sample => { + try { + await sample; + } catch (OperationCanceledException) { } +})); -return; - -static async Task SubscribeToStream(EventStoreClient client) { - #region subscribe-to-stream - await client.SubscribeToStreamAsync( - "some-stream", - FromStream.Start, - async (subscription, evnt, cancellationToken) => { - Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); - await HandleEvent(evnt); - } - ); +return; - #endregion subscribe-to-stream +IEnumerable YieldSamples() { + yield return SubscribeToStream(client, GetCT()); + yield return SubscribeToStreamFromPosition(client, GetCT()); + yield return SubscribeToStreamLive(client, GetCT()); + yield return SubscribeToStreamResolvingLinkTos(client, GetCT()); + yield return SubscribeToStreamSubscriptionDropped(client, GetCT()); + yield return SubscribeToAll(client, GetCT()); + yield return SubscribeToAllFromPosition(client, GetCT()); + yield return SubscribeToAllLive(client, GetCT()); + yield return SubscribeToAllSubscriptionDropped(client, GetCT()); + yield return OverridingUserCredentials(client, GetCT()); +} +static async Task SubscribeToStreamFromPosition(EventStoreClient client, CancellationToken ct) { #region subscribe-to-stream-from-position - await client.SubscribeToStreamAsync( + await using var subscription = client.SubscribeToStream( "some-stream", FromStream.After(StreamPosition.FromInt64(20)), - EventAppeared - ); + cancellationToken: ct); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + break; + } + } #endregion subscribe-to-stream-from-position +} +static async Task SubscribeToStreamLive(EventStoreClient client, CancellationToken ct) { #region subscribe-to-stream-live - await client.SubscribeToStreamAsync( + await using var subscription = client.SubscribeToStream( "some-stream", FromStream.End, - EventAppeared - ); + cancellationToken: ct); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + break; + } + } #endregion subscribe-to-stream-live +} +static async Task SubscribeToStreamResolvingLinkTos(EventStoreClient client, CancellationToken ct) { #region subscribe-to-stream-resolving-linktos - await client.SubscribeToStreamAsync( + await using var subscription = client.SubscribeToStream( "$et-myEventType", FromStream.Start, - EventAppeared, - true - ); + true, + cancellationToken: ct); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + break; + } + } #endregion subscribe-to-stream-resolving-linktos +} +static async Task SubscribeToStreamSubscriptionDropped(EventStoreClient client, CancellationToken ct) { #region subscribe-to-stream-subscription-dropped - var checkpoint = await ReadStreamCheckpointAsync(); - await client.SubscribeToStreamAsync( - "some-stream", - checkpoint is null ? FromStream.Start : FromStream.After(checkpoint.Value), - async (subscription, evnt, cancellationToken) => { - await HandleEvent(evnt); - checkpoint = evnt.OriginalEventNumber; - }, - subscriptionDropped: (subscription, reason, exception) => { - Console.WriteLine($"Subscription was dropped due to {reason}. {exception}"); - if (reason != SubscriptionDroppedReason.Disposed) - // Resubscribe if the client didn't stop the subscription - ResubscribeToStream(checkpoint); + var checkpoint = await ReadStreamCheckpointAsync() switch { + null => FromStream.Start, + var position => FromStream.After(position.Value) + }; + + Subscribe: + try { + await using var subscription = client.SubscribeToStream( + "some-stream", + checkpoint, + cancellationToken: ct); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + checkpoint = FromStream.After(evnt.OriginalEventNumber); + break; + } } - ); + } catch (OperationCanceledException) { + Console.WriteLine($"Subscription was canceled."); + } catch (ObjectDisposedException) { + Console.WriteLine($"Subscription was canceled by the user."); + } catch (Exception ex) { + Console.WriteLine($"Subscription was dropped: {ex}"); + goto Subscribe; + } #endregion subscribe-to-stream-subscription-dropped } -static async Task SubscribeToAll(EventStoreClient client) { +static async Task SubscribeToStream(EventStoreClient client, CancellationToken ct) { + #region subscribe-to-stream + + await using var subscription = client.SubscribeToStream( + "some-stream", + FromStream.Start, + cancellationToken: ct); + await foreach (var message in subscription.Messages.WithCancellation(ct)) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + break; + } + } + + #endregion subscribe-to-stream +} + +static async Task SubscribeToAll(EventStoreClient client, CancellationToken ct) { #region subscribe-to-all - await client.SubscribeToAllAsync( + await using var subscription = client.SubscribeToAll( FromAll.Start, - async (subscription, evnt, cancellationToken) => { - Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); - await HandleEvent(evnt); + cancellationToken: ct); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + break; } - ); + } #endregion subscribe-to-all +} +static async Task SubscribeToAllFromPosition(EventStoreClient client, CancellationToken ct) { #region subscribe-to-all-from-position var result = await client.AppendToStreamAsync( @@ -97,54 +165,98 @@ await client.SubscribeToAllAsync( StreamState.NoStream, new[] { new EventData(Uuid.NewUuid(), "-", ReadOnlyMemory.Empty) - } - ); + }); - await client.SubscribeToAllAsync( + await using var subscription = client.SubscribeToAll( FromAll.After(result.LogPosition), - EventAppeared - ); + cancellationToken: ct); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + break; + } + } #endregion subscribe-to-all-from-position +} +static async Task SubscribeToAllLive(EventStoreClient client, CancellationToken ct) { #region subscribe-to-all-live - await client.SubscribeToAllAsync( + var subscription = client.SubscribeToAll( FromAll.End, - EventAppeared - ); + cancellationToken: ct); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + break; + } + } #endregion subscribe-to-all-live +} +static async Task SubscribeToAllSubscriptionDropped(EventStoreClient client, CancellationToken ct) { #region subscribe-to-all-subscription-dropped - var checkpoint = await ReadCheckpointAsync(); - await client.SubscribeToAllAsync( - checkpoint is null ? FromAll.Start : FromAll.After(checkpoint.Value), - async (subscription, evnt, cancellationToken) => { - await HandleEvent(evnt); - checkpoint = evnt.OriginalPosition!.Value; - }, - subscriptionDropped: (subscription, reason, exception) => { - Console.WriteLine($"Subscription was dropped due to {reason}. {exception}"); - if (reason != SubscriptionDroppedReason.Disposed) - // Resubscribe if the client didn't stop the subscription - Resubscribe(checkpoint); + var checkpoint = await ReadCheckpointAsync() switch { + null => FromAll.Start, + var position => FromAll.After(position.Value) + }; + + Subscribe: + try { + await using var subscription = client.SubscribeToAll( + checkpoint, + cancellationToken: ct); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + if (evnt.OriginalPosition is not null) { + checkpoint = FromAll.After(evnt.OriginalPosition.Value); + } + + break; + } } - ); + } catch (OperationCanceledException) { + Console.WriteLine($"Subscription was canceled."); + } catch (ObjectDisposedException) { + Console.WriteLine($"Subscription was canceled by the user."); + } catch (Exception ex) { + Console.WriteLine($"Subscription was dropped: {ex}"); + goto Subscribe; + } #endregion subscribe-to-all-subscription-dropped } -static async Task SubscribeToFiltered(EventStoreClient client) { +static async Task SubscribeToFiltered(EventStoreClient client, CancellationToken ct) { #region stream-prefix-filtered-subscription var prefixStreamFilter = new SubscriptionFilterOptions(StreamFilter.Prefix("test-", "other-")); - await client.SubscribeToAllAsync( + await using var subscription = client.SubscribeToAll( FromAll.Start, - EventAppeared, - filterOptions: prefixStreamFilter - ); + filterOptions: prefixStreamFilter, + cancellationToken: ct); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + + break; + case StreamMessage.AllStreamCheckpointReached(var position): + Console.WriteLine($"Checkpoint reached: {position}"); + break; + } + } #endregion stream-prefix-filtered-subscription @@ -155,31 +267,33 @@ await client.SubscribeToAllAsync( #endregion stream-regex-filtered-subscription } -static async Task OverridingUserCredentials(EventStoreClient client) { +static async Task OverridingUserCredentials(EventStoreClient client, CancellationToken ct) { #region overriding-user-credentials - await client.SubscribeToAllAsync( + await using var subscription = client.SubscribeToAll( FromAll.Start, - EventAppeared, - userCredentials: new UserCredentials("admin", "changeit") - ); + userCredentials: new UserCredentials("admin", "changeit"), + cancellationToken: ct); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + + break; + } + } #endregion overriding-user-credentials } -static Task EventAppeared(StreamSubscription subscription, ResolvedEvent evnt, CancellationToken cancellationToken) => - Task.CompletedTask; - -static void SubscriptionDropped(StreamSubscription subscription, SubscriptionDroppedReason reason, Exception ex) { } - static Task HandleEvent(ResolvedEvent evnt) => Task.CompletedTask; -static void ResubscribeToStream(StreamPosition? checkpoint) { } - -static void Resubscribe(Position? checkpoint) { } - static Task ReadStreamCheckpointAsync() => Task.FromResult(new StreamPosition?()); static Task ReadCheckpointAsync() => - Task.FromResult(new Position?()); \ No newline at end of file + Task.FromResult(new Position?()); + +// ensures that samples exit in a timely manner on CI +static CancellationToken GetCT() => new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token; diff --git a/src/EventStore.Client.Common/AsyncStreamReaderExtensions.cs b/src/EventStore.Client.Common/AsyncStreamReaderExtensions.cs index 5ad598e4a..98f9de54d 100644 --- a/src/EventStore.Client.Common/AsyncStreamReaderExtensions.cs +++ b/src/EventStore.Client.Common/AsyncStreamReaderExtensions.cs @@ -1,3 +1,4 @@ +using System.Threading.Channels; using System.Runtime.CompilerServices; using Grpc.Core; @@ -12,4 +13,17 @@ public static async IAsyncEnumerable ReadAllAsync( while (await reader.MoveNext(cancellationToken).ConfigureAwait(false)) yield return reader.Current; } -} \ No newline at end of file + + public static async IAsyncEnumerable ReadAllAsync(this ChannelReader reader, [EnumeratorCancellation] CancellationToken cancellationToken = default) { +#if NET + await foreach (var item in reader.ReadAllAsync(cancellationToken)) + yield return item; +#else + while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) { + while (reader.TryRead(out T? item)) { + yield return item; + } + } +#endif + } +} diff --git a/src/EventStore.Client.Common/protos/streams.proto b/src/EventStore.Client.Common/protos/streams.proto index 2037038e5..ac599fb82 100644 --- a/src/EventStore.Client.Common/protos/streams.proto +++ b/src/EventStore.Client.Common/protos/streams.proto @@ -4,6 +4,7 @@ option java_package = "com.eventstore.dbclient.proto.streams"; import "shared.proto"; import "status.proto"; +import "google/protobuf/duration.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; @@ -98,8 +99,14 @@ message ReadResp { uint64 first_stream_position = 5; uint64 last_stream_position = 6; AllStreamPosition last_all_stream_position = 7; + CaughtUp caught_up = 8; + FellBehind fell_behind = 9; } + message CaughtUp {} + + message FellBehind {} + message ReadEvent { RecordedEvent event = 1; RecordedEvent link = 2; @@ -214,7 +221,10 @@ message BatchAppendReq { google.protobuf.Empty any = 4; google.protobuf.Empty stream_exists = 5; } - google.protobuf.Timestamp deadline = 6; + oneof deadline_option { + google.protobuf.Timestamp deadline_21_10_0 = 6; + google.protobuf.Duration deadline = 7; + } } message ProposedMessage { @@ -304,4 +314,3 @@ message TombstoneResp { uint64 prepare_position = 2; } } - diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs index 172f33787..d97fa0fc3 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs @@ -1,7 +1,7 @@ -using System; -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Channels; using EventStore.Client.PersistentSubscriptions; +using Grpc.Core; +using static EventStore.Client.PersistentSubscriptions.ReadResp.ContentOneofCase; namespace EventStore.Client { partial class EventStorePersistentSubscriptionsClient { @@ -11,7 +11,7 @@ partial class EventStorePersistentSubscriptionsClient { /// /// /// - [Obsolete("SubscribeAsync is no longer supported. Use SubscribeToStreamAsync with manual acks instead.", true)] + [Obsolete("SubscribeAsync is no longer supported. Use SubscribeToStream with manual acks instead.", true)] public async Task SubscribeAsync(string streamName, string groupName, Func eventAppeared, Action? subscriptionDropped = null, @@ -32,11 +32,29 @@ public async Task SubscribeAsync(string streamName, stri /// /// /// + [Obsolete("SubscribeToStreamAsync is no longer supported. Use SubscribeToStream with manual acks instead.", true)] public async Task SubscribeToStreamAsync(string streamName, string groupName, - Func eventAppeared, - Action? subscriptionDropped = null, - UserCredentials? userCredentials = null, int bufferSize = 10, - CancellationToken cancellationToken = default) { + Func eventAppeared, + Action? subscriptionDropped = null, + UserCredentials? userCredentials = null, int bufferSize = 10, + CancellationToken cancellationToken = default) { + return await PersistentSubscription + .Confirm(SubscribeToStream(streamName, groupName, bufferSize, userCredentials, cancellationToken), + eventAppeared, subscriptionDropped ?? delegate { }, _log, userCredentials, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Subscribes to a persistent subscription. Messages must be manually acknowledged. + /// + /// The name of the stream to read events from. + /// The name of the persistent subscription group. + /// The size of the buffer. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public PersistentSubscriptionResult SubscribeToStream(string streamName, string groupName, int bufferSize = 10, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { if (streamName == null) { throw new ArgumentNullException(nameof(streamName)); } @@ -45,10 +63,6 @@ public async Task SubscribeToStreamAsync(string streamNa throw new ArgumentNullException(nameof(groupName)); } - if (eventAppeared == null) { - throw new ArgumentNullException(nameof(eventAppeared)); - } - if (streamName == string.Empty) { throw new ArgumentException($"{nameof(streamName)} may not be empty.", nameof(streamName)); } @@ -61,17 +75,10 @@ public async Task SubscribeToStreamAsync(string streamNa throw new ArgumentOutOfRangeException(nameof(bufferSize)); } - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - - if (streamName == SystemStreams.AllStream && - !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { - throw new NotSupportedException("The server does not support persistent subscriptions to $all."); - } - var readOptions = new ReadReq.Types.Options { BufferSize = bufferSize, GroupName = groupName, - UuidOption = new ReadReq.Types.Options.Types.UUIDOption {Structured = new Empty()} + UuidOption = new ReadReq.Types.Options.Types.UUIDOption { Structured = new Empty() } }; if (streamName == SystemStreams.AllStream) { @@ -80,20 +87,322 @@ public async Task SubscribeToStreamAsync(string streamNa readOptions.StreamIdentifier = streamName; } - return await PersistentSubscription.Confirm(channelInfo.Channel, channelInfo.CallInvoker, Settings, userCredentials, readOptions, _log, eventAppeared, - subscriptionDropped ?? delegate { }, cancellationToken).ConfigureAwait(false); + return new PersistentSubscriptionResult(streamName, groupName, async ct => { + var channelInfo = await GetChannelInfo(ct).ConfigureAwait(false); + + if (streamName == SystemStreams.AllStream && + !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { + throw new NotSupportedException("The server does not support persistent subscriptions to $all."); + } + + return channelInfo.CallInvoker; + }, new() { Options = readOptions }, Settings, userCredentials, cancellationToken); } /// /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged /// + [Obsolete("SubscribeToAllAsync is no longer supported. Use SubscribeToAll with manual acks instead.", true)] public async Task SubscribeToAllAsync(string groupName, - Func eventAppeared, - Action? subscriptionDropped = null, - UserCredentials? userCredentials = null, int bufferSize = 10, - CancellationToken cancellationToken = default) => + Func eventAppeared, + Action? subscriptionDropped = null, + UserCredentials? userCredentials = null, int bufferSize = 10, + CancellationToken cancellationToken = default) => await SubscribeToStreamAsync(SystemStreams.AllStream, groupName, eventAppeared, subscriptionDropped, userCredentials, bufferSize, cancellationToken) .ConfigureAwait(false); + + /// + /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged. + /// + /// The name of the persistent subscription group. + /// The size of the buffer. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public PersistentSubscriptionResult SubscribeToAll(string groupName, int bufferSize = 10, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => + SubscribeToStream(SystemStreams.AllStream, groupName, bufferSize, userCredentials, cancellationToken); + + /// + public class PersistentSubscriptionResult : IAsyncEnumerable, IAsyncDisposable, IDisposable { + private const int MaxEventIdLength = 2000; + private readonly ReadReq _request; + private readonly Channel _channel; + private readonly CancellationTokenSource _cts; + private readonly CallOptions _callOptions; + + private AsyncDuplexStreamingCall? _call; + private int _messagesEnumerated; + + /// + /// The server-generated unique identifier for the subscription. + /// + public string? SubscriptionId { get; private set; } + + /// + /// The name of the stream to read events from. + /// + public string StreamName { get; } + + /// + /// The name of the persistent subscription group. + /// + public string GroupName { get; } + + /// + /// An . Do not enumerate more than once. + /// + public IAsyncEnumerable Messages { + get { + if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) { + throw new InvalidOperationException("Messages may only be enumerated once."); + } + + return GetMessages(); + + async IAsyncEnumerable GetMessages() { + try { + await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) { + if (message is PersistentSubscriptionMessage + .SubscriptionConfirmation(var subscriptionId)) { + SubscriptionId = subscriptionId; + } + + yield return message; + } + } finally { + _cts.Cancel(); + } + } + } + } + + internal PersistentSubscriptionResult(string streamName, string groupName, + Func> selectCallInvoker, + ReadReq request, EventStoreClientSettings settings, UserCredentials? userCredentials, + CancellationToken cancellationToken) { + StreamName = streamName; + GroupName = groupName; + + _request = request; + + _callOptions = EventStoreCallOptions.CreateStreaming(settings, userCredentials: userCredentials, + cancellationToken: cancellationToken); + + _channel = Channel.CreateBounded(ReadBoundedChannelOptions); + + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + _ = PumpMessages(); + + return; + + async Task PumpMessages() { + try { + var callInvoker = await selectCallInvoker(_cts.Token).ConfigureAwait(false); + var client = new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient( + callInvoker); + _call = client.Read(_callOptions); + + await _call.RequestStream.WriteAsync(_request).ConfigureAwait(false); + + await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token) + .ConfigureAwait(false)) { + await _channel.Writer.WriteAsync(response.ContentCase switch { + SubscriptionConfirmation => new PersistentSubscriptionMessage.SubscriptionConfirmation( + response.SubscriptionConfirmation.SubscriptionId), + Event => new PersistentSubscriptionMessage.Event(ConvertToResolvedEvent(response), + response.Event.CountCase switch { + ReadResp.Types.ReadEvent.CountOneofCase.RetryCount => response.Event.RetryCount, + _ => null + }), + _ => PersistentSubscriptionMessage.Unknown.Instance + }, _cts.Token).ConfigureAwait(false); + } + + _channel.Writer.TryComplete(); + } catch (Exception ex) { +#if NET48 + switch (ex) { + // The gRPC client for .NET 48 uses WinHttpHandler under the hood for sending HTTP requests. + // In certain scenarios, this can lead to exceptions of type WinHttpException being thrown. + // One such scenario is when the server abruptly closes the connection, which results in a WinHttpException with the error code 12030. + // Additionally, there are cases where the server response does not include the 'grpc-status' header. + // The absence of this header leads to an RpcException with the status code 'Cancelled' and the message "No grpc-status found on response". + // The switch statement below handles these specific exceptions and translates them into the appropriate + // PersistentSubscriptionDroppedByServerException exception. + case RpcException { StatusCode: StatusCode.Unavailable } rex1 when rex1.Status.Detail.Contains("WinHttpException: Error 12030"): + case RpcException { StatusCode: StatusCode.Cancelled } rex2 + when rex2.Status.Detail.Contains("No grpc-status found on response"): + ex = new PersistentSubscriptionDroppedByServerException(StreamName, GroupName, ex); + break; + } +#endif + if (ex is PersistentSubscriptionNotFoundException) { + await _channel.Writer.WriteAsync(PersistentSubscriptionMessage.NotFound.Instance, + cancellationToken).ConfigureAwait(false); + _channel.Writer.TryComplete(); + return; + } + _channel.Writer.TryComplete(ex); + } + } + } + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(params Uuid[] eventIds) => AckInternal(eventIds); + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(IEnumerable eventIds) => Ack(eventIds.ToArray()); + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(params ResolvedEvent[] resolvedEvents) => + Ack(Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId)); + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(IEnumerable resolvedEvents) => + Ack(resolvedEvents.Select(resolvedEvent => resolvedEvent.OriginalEvent.EventId)); + + + /// + /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). + /// + /// The to take. + /// A reason given. + /// The of the s to nak. There should not be more than 2000 to nak at a time. + /// The number of eventIds exceeded the limit of 2000. + public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) => + NackInternal(eventIds, action, reason); + + /// + /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). + /// + /// The to take. + /// A reason given. + /// The s to nak. There should not be more than 2000 to nak at a time. + /// The number of resolvedEvents exceeded the limit of 2000. + public Task Nack(PersistentSubscriptionNakEventAction action, string reason, + params ResolvedEvent[] resolvedEvents) => Nack(action, reason, + Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId)); + + private static ResolvedEvent ConvertToResolvedEvent(ReadResp response) => new( + ConvertToEventRecord(response.Event.Event)!, + ConvertToEventRecord(response.Event.Link), + response.Event.PositionCase switch { + ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => response.Event.CommitPosition, + _ => null + }); + + private Task AckInternal(params Uuid[] eventIds) { + if (eventIds.Length > MaxEventIdLength) { + throw new ArgumentException( + $"The number of eventIds exceeds the maximum length of {MaxEventIdLength}.", nameof(eventIds)); + } + + return _call is null + ? throw new InvalidOperationException() + : _call.RequestStream.WriteAsync(new ReadReq { + Ack = new ReadReq.Types.Ack { + Ids = { + Array.ConvertAll(eventIds, id => id.ToDto()) + } + } + }); + } + + private Task NackInternal(Uuid[] eventIds, PersistentSubscriptionNakEventAction action, string reason) { + if (eventIds.Length > MaxEventIdLength) { + throw new ArgumentException( + $"The number of eventIds exceeds the maximum length of {MaxEventIdLength}.", nameof(eventIds)); + } + + return _call is null + ? throw new InvalidOperationException() + : _call.RequestStream.WriteAsync(new ReadReq { + Nack = new ReadReq.Types.Nack { + Ids = { + Array.ConvertAll(eventIds, id => id.ToDto()) + }, + Action = action switch { + PersistentSubscriptionNakEventAction.Park => ReadReq.Types.Nack.Types.Action.Park, + PersistentSubscriptionNakEventAction.Retry => ReadReq.Types.Nack.Types.Action.Retry, + PersistentSubscriptionNakEventAction.Skip => ReadReq.Types.Nack.Types.Action.Skip, + PersistentSubscriptionNakEventAction.Stop => ReadReq.Types.Nack.Types.Action.Stop, + _ => ReadReq.Types.Nack.Types.Action.Unknown + }, + Reason = reason + } + }); + } + + private static EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) => + e is null + ? null + : new EventRecord( + e.StreamIdentifier!, + Uuid.FromDto(e.Id), + new StreamPosition(e.StreamRevision), + new Position(e.CommitPosition, e.PreparePosition), + e.Metadata, + e.Data.ToByteArray(), + e.CustomMetadata.ToByteArray()); + + /// + public async ValueTask DisposeAsync() { + await CastAndDispose(_cts).ConfigureAwait(false); + await CastAndDispose(_call).ConfigureAwait(false); + + return; + + static async Task CastAndDispose(IDisposable? resource) { + switch (resource) { + case null: + return; + case IAsyncDisposable resourceAsyncDisposable: + await resourceAsyncDisposable.DisposeAsync().ConfigureAwait(false); + break; + default: + resource.Dispose(); + break; + } + } + } + + + /// + public void Dispose() { + _cts.Dispose(); + _call?.Dispose(); + } + + /// + public async IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default) { + await foreach (var message in Messages.WithCancellation(cancellationToken)) { + if (message is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + yield return resolvedEvent; + } + } + } } } diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs index 1e14a5686..411de3e70 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.Encodings.Web; +using System.Threading.Channels; using Grpc.Core; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -11,6 +9,12 @@ namespace EventStore.Client { /// The client used to manage persistent subscriptions in the EventStoreDB. /// public sealed partial class EventStorePersistentSubscriptionsClient : EventStoreClientBase { + private static BoundedChannelOptions ReadBoundedChannelOptions = new (1) { + SingleReader = true, + SingleWriter = true, + AllowSynchronousContinuations = true + }; + private readonly ILogger _log; /// diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs b/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs index 56b8d305d..1e79b9363 100644 --- a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs +++ b/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using EventStore.Client.PersistentSubscriptions; using Grpc.Core; using Microsoft.Extensions.Logging; @@ -11,13 +6,14 @@ namespace EventStore.Client { /// /// Represents a persistent subscription connection. /// + [Obsolete] public class PersistentSubscription : IDisposable { + private readonly EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult _persistentSubscriptionResult; + private readonly IAsyncEnumerator _enumerator; private readonly Func _eventAppeared; - private readonly Action _subscriptionDropped; - private readonly IDisposable _disposable; - private readonly CancellationToken _cancellationToken; - private readonly AsyncDuplexStreamingCall _call; - private readonly ILogger _log; + private readonly Action _subscriptionDropped; + private readonly ILogger _log; + private readonly CancellationTokenSource _cts; private int _subscriptionDroppedInvoked; @@ -27,47 +23,43 @@ public class PersistentSubscription : IDisposable { public string SubscriptionId { get; } internal static async Task Confirm( - ChannelBase channel, CallInvoker callInvoker, EventStoreClientSettings settings, - UserCredentials? userCredentials, ReadReq.Types.Options options, ILogger log, + EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult persistentSubscriptionResult, Func eventAppeared, Action subscriptionDropped, - CancellationToken cancellationToken = default) { - - var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - var call = new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(callInvoker) - .Read(EventStoreCallOptions.CreateStreaming(settings, userCredentials: userCredentials, - cancellationToken: cts.Token)); - - await call.RequestStream.WriteAsync(new ReadReq { - Options = options - }).ConfigureAwait(false); - - if (await call.ResponseStream.MoveNext(cancellationToken).ConfigureAwait(false) && - call.ResponseStream.Current.ContentCase == ReadResp.ContentOneofCase.SubscriptionConfirmation) - return new PersistentSubscription(call, log, eventAppeared, subscriptionDropped, cts.Token, cts); - - call.Dispose(); - cts.Dispose(); - throw new InvalidOperationException("Subscription could not be confirmed."); + ILogger log, UserCredentials? userCredentials, CancellationToken cancellationToken = default) { + var enumerator = persistentSubscriptionResult + .Messages + .GetAsyncEnumerator(cancellationToken); + + var result = await enumerator.MoveNextAsync(cancellationToken).ConfigureAwait(false); + + return (result, enumerator.Current) switch { + (true, PersistentSubscriptionMessage.SubscriptionConfirmation (var subscriptionId)) => + new PersistentSubscription(persistentSubscriptionResult, enumerator, subscriptionId, eventAppeared, + subscriptionDropped, log, cancellationToken), + (true, PersistentSubscriptionMessage.NotFound) => + throw new PersistentSubscriptionNotFoundException(persistentSubscriptionResult.StreamName, + persistentSubscriptionResult.GroupName), + _ => throw new InvalidOperationException("Subscription could not be confirmed.") + }; } // PersistentSubscription takes responsibility for disposing the call and the disposable private PersistentSubscription( - AsyncDuplexStreamingCall call, - ILogger log, + EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult persistentSubscriptionResult, + IAsyncEnumerator enumerator, string subscriptionId, Func eventAppeared, - Action subscriptionDropped, - CancellationToken cancellationToken, - IDisposable disposable) { - _call = call; - _eventAppeared = eventAppeared; + Action subscriptionDropped, ILogger log, + CancellationToken cancellationToken) { + _persistentSubscriptionResult = persistentSubscriptionResult; + _enumerator = enumerator; + SubscriptionId = subscriptionId; + _eventAppeared = eventAppeared; _subscriptionDropped = subscriptionDropped; - _cancellationToken = cancellationToken; - _disposable = disposable; - _log = log; - SubscriptionId = call.ResponseStream.Current.SubscriptionConfirmation.SubscriptionId; - Task.Run(Subscribe); + _log = log; + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + Task.Run(Subscribe, _cts.Token); } /// @@ -75,13 +67,7 @@ private PersistentSubscription( /// /// There is no need to ack a message if you have Auto Ack enabled. /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. - public Task Ack(params Uuid[] eventIds) { - if (eventIds.Length > 2000) { - throw new ArgumentException(); - } - - return AckInternal(eventIds); - } + public Task Ack(params Uuid[] eventIds) => AckInternal(eventIds); /// /// Acknowledge that a message has completed processing (this will tell the server it has been processed). @@ -114,13 +100,7 @@ public Task Ack(IEnumerable resolvedEvents) => /// A reason given. /// The of the s to nak. There should not be more than 2000 to nak at a time. /// The number of eventIds exceeded the limit of 2000. - public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) { - if (eventIds.Length > 2000) { - throw new ArgumentException(); - } - - return NackInternal(eventIds, action, reason); - } + public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) => NackInternal(eventIds, action, reason); /// /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). @@ -130,7 +110,7 @@ public Task Nack(PersistentSubscriptionNakEventAction action, string reason, par /// The s to nak. There should not be more than 2000 to nak at a time. /// The number of resolvedEvents exceeded the limit of 2000. public Task Nack(PersistentSubscriptionNakEventAction action, string reason, - params ResolvedEvent[] resolvedEvents) => + params ResolvedEvent[] resolvedEvents) => Nack(action, reason, Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId)); @@ -141,12 +121,21 @@ private async Task Subscribe() { _log.LogDebug("Persistent Subscription {subscriptionId} confirmed.", SubscriptionId); try { - await foreach (var response in _call.ResponseStream.ReadAllAsync(_cancellationToken).ConfigureAwait(false)) { - if (response.ContentCase != ReadResp.ContentOneofCase.Event) { + while (await _enumerator.MoveNextAsync(_cts.Token).ConfigureAwait(false)) { + if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, var retryCount)) { continue; } - var resolvedEvent = ConvertToResolvedEvent(response); + if (_enumerator.Current is PersistentSubscriptionMessage.NotFound) { + if (_subscriptionDroppedInvoked != 0) { + return; + } + SubscriptionDropped(SubscriptionDroppedReason.ServerError, + new PersistentSubscriptionNotFoundException( + _persistentSubscriptionResult.StreamName, _persistentSubscriptionResult.GroupName)); + return; + } + _log.LogTrace( "Persistent Subscription {subscriptionId} received event {streamName}@{streamRevision} {position}", SubscriptionId, resolvedEvent.OriginalEvent.EventStreamId, @@ -156,13 +145,8 @@ private async Task Subscribe() { await _eventAppeared( this, resolvedEvent, - response.Event.CountCase switch { - ReadResp.Types.ReadEvent.CountOneofCase.RetryCount => response.Event - .RetryCount, - _ => default - }, - _cancellationToken).ConfigureAwait(false); - + retryCount, + _cts.Token).ConfigureAwait(false); } catch (Exception ex) when (ex is ObjectDisposedException or OperationCanceledException) { if (_subscriptionDroppedInvoked != 0) { return; @@ -186,23 +170,6 @@ await _eventAppeared( } } catch (Exception ex) { if (_subscriptionDroppedInvoked == 0) { -#if NET48 - switch (ex) { - // The gRPC client for .NET 48 uses WinHttpHandler under the hood for sending HTTP requests. - // In certain scenarios, this can lead to exceptions of type WinHttpException being thrown. - // One such scenario is when the server abruptly closes the connection, which results in a WinHttpException with the error code 12030. - // Additionally, there are cases where the server response does not include the 'grpc-status' header. - // The absence of this header leads to an RpcException with the status code 'Cancelled' and the message "No grpc-status found on response". - // The switch statement below handles these specific exceptions and translates them into the appropriate - // PersistentSubscriptionDroppedByServerException exception. The downside of this approach is that it does not return the stream name - // and group name. - case RpcException { StatusCode: StatusCode.Unavailable } rex1 when rex1.Status.Detail.Contains("WinHttpException: Error 12030"): - case RpcException { StatusCode: StatusCode.Cancelled } rex2 - when rex2.Status.Detail.Contains("No grpc-status found on response"): - ex = new PersistentSubscriptionDroppedByServerException("", "", ex); - break; - } -#endif _log.LogError(ex, "Persistent Subscription {subscriptionId} was dropped because an error occurred on the server.", SubscriptionId); @@ -216,26 +183,6 @@ when rex2.Status.Detail.Contains("No grpc-status found on response"): SubscriptionDropped(SubscriptionDroppedReason.ServerError); } } - - ResolvedEvent ConvertToResolvedEvent(ReadResp response) => new( - ConvertToEventRecord(response.Event.Event)!, - ConvertToEventRecord(response.Event.Link), - response.Event.PositionCase switch { - ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => response.Event.CommitPosition, - _ => null - }); - - EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) => - e == null - ? null - : new EventRecord( - e.StreamIdentifier!, - Uuid.FromDto(e.Id), - new StreamPosition(e.StreamRevision), - new Position(e.CommitPosition, e.PreparePosition), - e.Metadata, - e.Data.ToByteArray(), - e.CustomMetadata.ToByteArray()); } private void SubscriptionDropped(SubscriptionDroppedReason reason, Exception? ex = null) { @@ -246,35 +193,14 @@ private void SubscriptionDropped(SubscriptionDroppedReason reason, Exception? ex try { _subscriptionDropped.Invoke(this, reason, ex); } finally { - _call.Dispose(); - _disposable.Dispose(); + _persistentSubscriptionResult.Dispose(); + _cts.Dispose(); } } - private Task AckInternal(params Uuid[] ids) => - _call.RequestStream.WriteAsync(new ReadReq { - Ack = new ReadReq.Types.Ack { - Ids = { - Array.ConvertAll(ids, id => id.ToDto()) - } - } - }); + private Task AckInternal(params Uuid[] ids) => _persistentSubscriptionResult.Ack(ids); private Task NackInternal(Uuid[] ids, PersistentSubscriptionNakEventAction action, string reason) => - _call!.RequestStream.WriteAsync(new ReadReq { - Nack = new ReadReq.Types.Nack { - Ids = { - Array.ConvertAll(ids, id => id.ToDto()) - }, - Action = action switch { - PersistentSubscriptionNakEventAction.Park => ReadReq.Types.Nack.Types.Action.Park, - PersistentSubscriptionNakEventAction.Retry => ReadReq.Types.Nack.Types.Action.Retry, - PersistentSubscriptionNakEventAction.Skip => ReadReq.Types.Nack.Types.Action.Skip, - PersistentSubscriptionNakEventAction.Stop => ReadReq.Types.Nack.Types.Action.Stop, - _ => ReadReq.Types.Nack.Types.Action.Unknown - }, - Reason = reason - } - }); + _persistentSubscriptionResult.Nack(action, reason, ids); } -} \ No newline at end of file +} diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionMessage.cs b/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionMessage.cs new file mode 100644 index 000000000..eabde9b62 --- /dev/null +++ b/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionMessage.cs @@ -0,0 +1,33 @@ +namespace EventStore.Client { + /// + /// The base record of all stream messages. + /// + public abstract record PersistentSubscriptionMessage { + /// + /// A that represents a . + /// + /// The . + /// The number of times the has been retried. + public record Event(ResolvedEvent ResolvedEvent, int? RetryCount) : PersistentSubscriptionMessage; + + /// + /// A representing a stream that was not found. + /// + public record NotFound : PersistentSubscriptionMessage { + internal static readonly NotFound Instance = new(); + } + + /// + /// A indicating that the subscription is ready to send additional messages. + /// + /// The unique identifier of the subscription. + public record SubscriptionConfirmation(string SubscriptionId) : PersistentSubscriptionMessage; + + /// + /// A that could not be identified, usually indicating a lower client compatibility level than the server supports. + /// + public record Unknown : PersistentSubscriptionMessage { + internal static readonly Unknown Instance = new(); + } + } +} diff --git a/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj b/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj index a9c2969f2..4aa697465 100644 --- a/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj +++ b/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj @@ -3,9 +3,6 @@ The GRPC client API for Event Store Streams. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - - - diff --git a/src/EventStore.Client.Streams/EventStoreClient.Append.cs b/src/EventStore.Client.Streams/EventStoreClient.Append.cs index 2cf59db32..9d0842855 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Append.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Append.cs @@ -293,7 +293,7 @@ private async Task Send() { if (call is null) throw new NotSupportedException("Server does not support batch append"); - await foreach (var appendRequest in ReadAllAsync(_channel.Reader, _cancellationToken) + await foreach (var appendRequest in _channel.Reader.ReadAllAsync(_cancellationToken) .ConfigureAwait(false)) { await call.RequestStream.WriteAsync(appendRequest).ConfigureAwait(false); } @@ -364,18 +364,5 @@ public void Dispose() { _channel.Writer.TryComplete(); } } - - private static async IAsyncEnumerable ReadAllAsync(ChannelReader reader, [EnumeratorCancellation] CancellationToken cancellationToken = default) { -#if NET - await foreach (var item in reader.ReadAllAsync(cancellationToken)) - yield return item; -#else - while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) { - while (reader.TryRead(out T? item)) { - yield return item; - } - } -#endif - } } } diff --git a/src/EventStore.Client.Streams/EventStoreClient.Read.cs b/src/EventStore.Client.Streams/EventStoreClient.Read.cs index 92dad2c27..d79b83bc6 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Read.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Read.cs @@ -135,7 +135,8 @@ async IAsyncEnumerable GetMessages() { } try { - await foreach (var message in ReadAllAsync(_channel.Reader).ConfigureAwait(false)) { + await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token) + .ConfigureAwait(false)) { if (message is StreamMessage.LastAllStreamPosition(var position)) { LastPosition = position; } @@ -155,11 +156,7 @@ internal ReadAllStreamResult(Func> selectCa var callOptions = EventStoreCallOptions.CreateStreaming(settings, deadline, userCredentials, cancellationToken); - _channel = Channel.CreateBounded(new BoundedChannelOptions(1) { - SingleReader = true, - SingleWriter = true, - AllowSynchronousContinuations = true - }); + _channel = Channel.CreateBounded(ReadBoundedChannelOptions); _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var linkedCancellationToken = _cts.Token; @@ -199,7 +196,7 @@ public async IAsyncEnumerator GetAsyncEnumerator( CancellationToken cancellationToken = default) { try { - await foreach (var message in ReadAllAsync(_channel.Reader, cancellationToken).ConfigureAwait(false)) { + await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { if (message is not StreamMessage.Event e) { continue; } @@ -298,7 +295,7 @@ async IAsyncEnumerable GetMessages() { } try { - await foreach (var message in ReadAllAsync(_channel.Reader).ConfigureAwait(false)) { + await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { switch (message) { case StreamMessage.FirstStreamPosition(var streamPosition): FirstStreamPosition = streamPosition; @@ -330,11 +327,7 @@ internal ReadStreamResult(Func> selectCallI var callOptions = EventStoreCallOptions.CreateStreaming(settings, deadline, userCredentials, cancellationToken); - _channel = Channel.CreateBounded(new BoundedChannelOptions(1) { - SingleReader = true, - SingleWriter = true, - AllowSynchronousContinuations = true - }); + _channel = Channel.CreateBounded(ReadBoundedChannelOptions); StreamName = request.Options.Stream.StreamIdentifier!; @@ -396,7 +389,7 @@ public async IAsyncEnumerator GetAsyncEnumerator( CancellationToken cancellationToken = default) { try { - await foreach (var message in ReadAllAsync(_channel.Reader, cancellationToken).ConfigureAwait(false)) { + await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { if (message is StreamMessage.NotFound) { throw new StreamNotFoundException(StreamName); } @@ -412,55 +405,7 @@ public async IAsyncEnumerator GetAsyncEnumerator( } } } - - private async IAsyncEnumerable<(SubscriptionConfirmation, Position?, ResolvedEvent)> ReadInternal( - ReadReq request, - UserCredentials? userCredentials, - [EnumeratorCancellation] CancellationToken cancellationToken) { - if (request.Options.CountOptionCase == ReadReq.Types.Options.CountOptionOneofCase.Count && - request.Options.Count <= 0) { - throw new ArgumentOutOfRangeException("count"); - } - - if (request.Options.Filter == null) { - request.Options.NoFilter = new Empty(); - } - - request.Options.UuidOption = new ReadReq.Types.Options.Types.UUIDOption {Structured = new Empty()}; - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - using var call = new Streams.Streams.StreamsClient( - channelInfo.CallInvoker).Read(request, - EventStoreCallOptions.CreateStreaming(Settings, userCredentials: userCredentials, - cancellationToken: cts.Token)); - - await foreach (var e in call.ResponseStream - .ReadAllAsync(cts.Token) - .Select(ConvertToItem) - .WithCancellation(cts.Token) - .ConfigureAwait(false)) { - if (e.HasValue) { - yield return e.Value; - } - } - } - - private static (SubscriptionConfirmation, Position?, ResolvedEvent)? ConvertToItem(ReadResp response) => - response.ContentCase switch { - Confirmation => ( - new SubscriptionConfirmation(response.Confirmation.SubscriptionId), null, default), - Event => (SubscriptionConfirmation.None, - null, - ConvertToResolvedEvent(response.Event)), - Checkpoint => (SubscriptionConfirmation.None, - new Position(response.Checkpoint.CommitPosition, response.Checkpoint.PreparePosition), - default), - _ => null - }; - + private static ResolvedEvent ConvertToResolvedEvent(ReadResp.Types.ReadEvent readEvent) => new ResolvedEvent( ConvertToEventRecord(readEvent.Event)!, diff --git a/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs b/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs index 47ea634f1..78013f31c 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs @@ -1,7 +1,7 @@ -using System; -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Channels; using EventStore.Client.Streams; +using Grpc.Core; +using static EventStore.Client.Streams.ReadResp.ContentOneofCase; namespace EventStore.Client { public partial class EventStoreClient { @@ -16,6 +16,7 @@ public partial class EventStoreClient { /// The optional user credentials to perform operation with. /// The optional . /// + [Obsolete("SubscribeToAllAsync is no longer supported. Use SubscribeToAll instead.", true)] public Task SubscribeToAllAsync( FromAll start, Func eventAppeared, @@ -23,16 +24,38 @@ public Task SubscribeToAllAsync( Action? subscriptionDropped = default, SubscriptionFilterOptions? filterOptions = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => StreamSubscription.Confirm(ReadInternal(new ReadReq { - Options = new ReadReq.Types.Options { - ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards, - ResolveLinks = resolveLinkTos, - All = ReadReq.Types.Options.Types.AllOptions.FromSubscriptionPosition(start), - Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(), - Filter = GetFilterOptions(filterOptions)! - } - }, userCredentials, cancellationToken), eventAppeared, subscriptionDropped, _log, - filterOptions?.CheckpointReached, cancellationToken); + CancellationToken cancellationToken = default) => StreamSubscription.Confirm( + SubscribeToAll(start, resolveLinkTos, filterOptions, userCredentials, cancellationToken), + eventAppeared, subscriptionDropped, _log, filterOptions?.CheckpointReached, + cancellationToken: cancellationToken); + + /// + /// Subscribes to all events. + /// + /// A (exclusive of) to start the subscription from. + /// Whether to resolve LinkTo events automatically. + /// The optional to apply. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public StreamSubscriptionResult SubscribeToAll( + FromAll start, + bool resolveLinkTos = false, + SubscriptionFilterOptions? filterOptions = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => new(async _ => { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + return channelInfo.CallInvoker; + }, new ReadReq { + Options = new ReadReq.Types.Options { + ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards, + ResolveLinks = resolveLinkTos, + All = ReadReq.Types.Options.Types.AllOptions.FromSubscriptionPosition(start), + Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(), + Filter = GetFilterOptions(filterOptions)!, + UuidOption = new() { Structured = new() } + } + }, Settings, userCredentials, cancellationToken); /// /// Subscribes to a stream from a checkpoint. @@ -45,20 +68,184 @@ public Task SubscribeToAllAsync( /// The optional user credentials to perform operation with. /// The optional . /// + [Obsolete("SubscribeToStreamAsync is no longer supported. Use SubscribeToStream instead.", true)] public Task SubscribeToStreamAsync(string streamName, + FromStream start, + Func eventAppeared, + bool resolveLinkTos = false, + Action? subscriptionDropped = default, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => StreamSubscription.Confirm( + SubscribeToStream(streamName, start, resolveLinkTos, userCredentials, cancellationToken), + eventAppeared, subscriptionDropped, _log, cancellationToken: cancellationToken); + + /// + /// Subscribes to a stream from a checkpoint. + /// + /// A (exclusive of) to start the subscription from. + /// The name of the stream to read events from. + /// Whether to resolve LinkTo events automatically. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public StreamSubscriptionResult SubscribeToStream( + string streamName, FromStream start, - Func eventAppeared, bool resolveLinkTos = false, - Action? subscriptionDropped = default, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => StreamSubscription.Confirm(ReadInternal(new ReadReq { - Options = new ReadReq.Types.Options { - ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards, - ResolveLinks = resolveLinkTos, - Stream = ReadReq.Types.Options.Types.StreamOptions.FromSubscriptionPosition(streamName, start), - Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions() + CancellationToken cancellationToken = default) => new(async _ => { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + return channelInfo.CallInvoker; + }, new ReadReq { + Options = new ReadReq.Types.Options { + ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards, + ResolveLinks = resolveLinkTos, + Stream = ReadReq.Types.Options.Types.StreamOptions.FromSubscriptionPosition(streamName, start), + Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(), + UuidOption = new() { Structured = new() } + } + }, Settings, userCredentials, cancellationToken); + + /// + /// A class that represents the result of a subscription operation. You may either enumerate this instance directly or . Do not enumerate more than once. + /// + public class StreamSubscriptionResult : IAsyncEnumerable, IAsyncDisposable, IDisposable { + private readonly ReadReq _request; + private readonly Channel _channel; + private readonly CancellationTokenSource _cts; + private readonly CallOptions _callOptions; + private AsyncServerStreamingCall? _call; + + private int _messagesEnumerated; + + /// + /// The server-generated unique identifier for the subscription. + /// + public string? SubscriptionId { get; private set; } + + /// + /// An . Do not enumerate more than once. + /// + public IAsyncEnumerable Messages { + get { + if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) { + throw new InvalidOperationException("Messages may only be enumerated once."); + } + + return GetMessages(); + + async IAsyncEnumerable GetMessages() { + try { + await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) { + if (message is StreamMessage.SubscriptionConfirmation(var subscriptionId)) { + SubscriptionId = subscriptionId; + } + + yield return message; + } + } finally { + _cts.Cancel(); + } + } } - }, userCredentials, cancellationToken), eventAppeared, subscriptionDropped, _log, - cancellationToken: cancellationToken); + } + + internal StreamSubscriptionResult(Func> selectCallInvoker, + ReadReq request, EventStoreClientSettings settings, UserCredentials? userCredentials, + CancellationToken cancellationToken) { + _request = request; + _callOptions = EventStoreCallOptions.CreateStreaming(settings, userCredentials: userCredentials, + cancellationToken: cancellationToken); + + _channel = Channel.CreateBounded(ReadBoundedChannelOptions); + + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + if (_request.Options.FilterOptionCase == ReadReq.Types.Options.FilterOptionOneofCase.None) { + _request.Options.NoFilter = new(); + } + + _ = PumpMessages(); + + return; + + async Task PumpMessages() { + try { + var callInvoker = await selectCallInvoker(_cts.Token).ConfigureAwait(false); + var client = new Streams.Streams.StreamsClient(callInvoker); + _call = client.Read(_request, _callOptions); + await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token) + .ConfigureAwait(false)) { + await _channel.Writer.WriteAsync(response.ContentCase switch { + Confirmation => new StreamMessage.SubscriptionConfirmation( + response.Confirmation.SubscriptionId), + Event => new StreamMessage.Event(ConvertToResolvedEvent(response.Event)), + FirstStreamPosition => new StreamMessage.FirstStreamPosition( + new StreamPosition(response.FirstStreamPosition)), + LastStreamPosition => new StreamMessage.LastStreamPosition( + new StreamPosition(response.LastStreamPosition)), + LastAllStreamPosition => new StreamMessage.LastAllStreamPosition( + new Position(response.LastAllStreamPosition.CommitPosition, + response.LastAllStreamPosition.PreparePosition)), + Checkpoint => new StreamMessage.AllStreamCheckpointReached( + new Position(response.Checkpoint.CommitPosition, + response.Checkpoint.PreparePosition)), + CaughtUp => StreamMessage.CaughtUp.Instance, + FellBehind => StreamMessage.FellBehind.Instance, + _ => StreamMessage.Unknown.Instance + }, _cts.Token).ConfigureAwait(false); + } + + _channel.Writer.Complete(); + } catch (Exception ex) { + _channel.Writer.TryComplete(ex); + } + } + } + + /// + public async ValueTask DisposeAsync() { + await CastAndDispose(_cts).ConfigureAwait(false); + await CastAndDispose(_call).ConfigureAwait(false); + + return; + + static async ValueTask CastAndDispose(IDisposable? resource) { + switch (resource) { + case null: + return; + case IAsyncDisposable resourceAsyncDisposable: + await resourceAsyncDisposable.DisposeAsync().ConfigureAwait(false); + break; + default: + resource.Dispose(); + break; + } + } + } + + /// + public void Dispose() { + _cts.Dispose(); + _call?.Dispose(); + } + + /// + public async IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default) { + try { + await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) { + if (message is not StreamMessage.Event e) { + continue; + } + + yield return e.ResolvedEvent; + } + } finally { + _cts.Cancel(); + } + } + } } } diff --git a/src/EventStore.Client.Streams/EventStoreClient.cs b/src/EventStore.Client.Streams/EventStoreClient.cs index e9d7fd85c..361e6e2d4 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Channels; using EventStore.Client.Streams; using Grpc.Core; using Microsoft.Extensions.Logging; @@ -23,6 +19,12 @@ public sealed partial class EventStoreClient : EventStoreClientBase { }, }; + private static BoundedChannelOptions ReadBoundedChannelOptions = new (1) { + SingleReader = true, + SingleWriter = true, + AllowSynchronousContinuations = true + }; + private readonly ILogger _log; private Lazy _streamAppenderLazy; private StreamAppender _streamAppender => _streamAppenderLazy.Value; diff --git a/src/EventStore.Client.Streams/StreamMessage.cs b/src/EventStore.Client.Streams/StreamMessage.cs index f6b564e71..4f1a87559 100644 --- a/src/EventStore.Client.Streams/StreamMessage.cs +++ b/src/EventStore.Client.Streams/StreamMessage.cs @@ -41,6 +41,38 @@ public record LastStreamPosition(StreamPosition StreamPosition) : StreamMessage; /// The . public record LastAllStreamPosition(Position Position) : StreamMessage; + /// + /// A indicating that the subscription is ready to send additional messages. + /// + /// The unique identifier of the subscription. + public record SubscriptionConfirmation(string SubscriptionId) : StreamMessage; + + /// + /// A indicating that a checkpoint has been reached. + /// + /// The . + public record AllStreamCheckpointReached(Position Position) : StreamMessage; + + /// + /// A indicating that a checkpoint has been reached. + /// + /// The . + public record StreamCheckpointReached(StreamPosition StreamPosition) : StreamMessage; + + /// + /// A indicating that the subscription is live. + /// + public record CaughtUp : StreamMessage { + internal static readonly CaughtUp Instance = new(); + } + + /// + /// A indicating that the subscription has switched to catch up mode. + /// + public record FellBehind : StreamMessage { + internal static readonly FellBehind Instance = new(); + } + /// /// A that could not be identified, usually indicating a lower client compatibility level than the server supports. /// diff --git a/src/EventStore.Client.Streams/StreamSubscription.cs b/src/EventStore.Client.Streams/StreamSubscription.cs index db21c551a..9019eda64 100644 --- a/src/EventStore.Client.Streams/StreamSubscription.cs +++ b/src/EventStore.Client.Streams/StreamSubscription.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using Grpc.Core; using Microsoft.Extensions.Logging; @@ -9,13 +5,15 @@ namespace EventStore.Client { /// /// A class representing a . /// + [Obsolete] public class StreamSubscription : IDisposable { - private readonly IAsyncEnumerable _events; + private readonly EventStoreClient.StreamSubscriptionResult _subscription; + private readonly IAsyncEnumerator _messages; private readonly Func _eventAppeared; private readonly Func _checkpointReached; private readonly Action? _subscriptionDropped; private readonly ILogger _log; - private readonly CancellationTokenSource _disposed; + private readonly CancellationTokenSource _cts; private int _subscriptionDroppedInvoked; /// @@ -23,58 +21,69 @@ public class StreamSubscription : IDisposable { /// public string SubscriptionId { get; } - internal static async Task Confirm( - IAsyncEnumerable<(SubscriptionConfirmation confirmation, Position?, ResolvedEvent)> read, + internal static async Task Confirm(EventStoreClient.StreamSubscriptionResult subscription, Func eventAppeared, Action? subscriptionDropped, ILogger log, Func? checkpointReached = null, CancellationToken cancellationToken = default) { + var messages = subscription.Messages; - var enumerator = read.GetAsyncEnumerator(cancellationToken); - if (await enumerator.MoveNextAsync(cancellationToken).ConfigureAwait(false) && - enumerator.Current.confirmation != SubscriptionConfirmation.None) - return new StreamSubscription(enumerator, eventAppeared, subscriptionDropped, log, - checkpointReached, cancellationToken); - throw new InvalidOperationException($"Subscription to {enumerator} could not be confirmed."); + var enumerator = messages.GetAsyncEnumerator(cancellationToken); + if (!await enumerator.MoveNextAsync().ConfigureAwait(false) || + enumerator.Current is not StreamMessage.SubscriptionConfirmation(var subscriptionId)) { + throw new InvalidOperationException($"Subscription to {enumerator} could not be confirmed."); + } + + return new StreamSubscription(subscription, enumerator, subscriptionId, eventAppeared, subscriptionDropped, + log, checkpointReached, cancellationToken); } - private StreamSubscription( - IAsyncEnumerator<(SubscriptionConfirmation confirmation, Position?, ResolvedEvent)> events, + private StreamSubscription(EventStoreClient.StreamSubscriptionResult subscription, + IAsyncEnumerator messages, string subscriptionId, Func eventAppeared, Action? subscriptionDropped, ILogger log, Func? checkpointReached, CancellationToken cancellationToken = default) { - _disposed = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - _events = new Enumerable(events, CheckpointReached, _disposed.Token); + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _subscription = subscription; + _messages = messages; _eventAppeared = eventAppeared; - _checkpointReached = checkpointReached ?? ((_, __, ct) => Task.CompletedTask); + _checkpointReached = checkpointReached ?? ((_, _, _) => Task.CompletedTask); _subscriptionDropped = subscriptionDropped; _log = log; _subscriptionDroppedInvoked = 0; - SubscriptionId = events.Current.confirmation.SubscriptionId; + SubscriptionId = subscriptionId; - Task.Run(Subscribe); - } + _log.LogDebug("Subscription {subscriptionId} confirmed.", SubscriptionId); - private Task CheckpointReached(Position position) => _checkpointReached(this, position, _disposed.Token); + Task.Run(Subscribe, cancellationToken); + } private async Task Subscribe() { - _log.LogDebug("Subscription {subscriptionId} confirmed.", SubscriptionId); - using var _ = _disposed; + using var _ = _cts; try { - await foreach (var resolvedEvent in _events.ConfigureAwait(false)) { + while (await _messages.MoveNextAsync().ConfigureAwait(false)) { + var message = _messages.Current; try { - _log.LogTrace( - "Subscription {subscriptionId} received event {streamName}@{streamRevision} {position}", - SubscriptionId, - resolvedEvent.OriginalEvent.EventStreamId, - resolvedEvent.OriginalEvent.EventNumber, - resolvedEvent.OriginalEvent.Position - ); - await _eventAppeared(this, resolvedEvent, _disposed.Token).ConfigureAwait(false); + switch (message) { + case StreamMessage.Event(var resolvedEvent): + _log.LogTrace( + "Subscription {subscriptionId} received event {streamName}@{streamRevision} {position}", + SubscriptionId, + resolvedEvent.OriginalEvent.EventStreamId, + resolvedEvent.OriginalEvent.EventNumber, + resolvedEvent.OriginalEvent.Position + ); + await _eventAppeared(this, resolvedEvent, _cts.Token).ConfigureAwait(false); + break; + case StreamMessage.AllStreamCheckpointReached (var position): + await _checkpointReached(this, position, _cts.Token) + .ConfigureAwait(false); + break; + } } catch (Exception ex) when (ex is ObjectDisposedException or OperationCanceledException) { if (_subscriptionDroppedInvoked != 0) { @@ -104,11 +113,10 @@ private async Task Subscribe() { } catch (RpcException ex) when (ex.Status.StatusCode == StatusCode.Cancelled && ex.Status.Detail.Contains("Call canceled by the client.")) { _log.LogInformation( - "Subscription {subscriptionId} was dropped because cancellation was requested by the client.", + "Subscription {subscriptionId} was dropped because cancellation was requested by the client.", SubscriptionId); - SubscriptionDropped(SubscriptionDroppedReason.Disposed, ex); - } - catch (Exception ex) { + SubscriptionDropped(SubscriptionDroppedReason.Disposed, ex); + } catch (Exception ex) { if (_subscriptionDroppedInvoked == 0) { _log.LogError(ex, "Subscription {subscriptionId} was dropped because an error occurred on the server.", @@ -129,80 +137,8 @@ private void SubscriptionDropped(SubscriptionDroppedReason reason, Exception? ex try { _subscriptionDropped?.Invoke(this, reason, ex); } finally { - _disposed.Dispose(); - } - } - - private class Enumerable : IAsyncEnumerable { - private readonly IAsyncEnumerator<(SubscriptionConfirmation, Position?, ResolvedEvent)> _inner; - private readonly Func _checkpointReached; - private readonly CancellationToken _cancellationToken; - - public Enumerable(IAsyncEnumerator<(SubscriptionConfirmation, Position?, ResolvedEvent)> inner, - Func checkpointReached, CancellationToken cancellationToken) { - if (inner == null) { - throw new ArgumentNullException(nameof(inner)); - } - - if (checkpointReached == null) { - throw new ArgumentNullException(nameof(checkpointReached)); - } - - _inner = inner; - _checkpointReached = checkpointReached; - _cancellationToken = cancellationToken; - } - - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) - => new Enumerator(_inner, _checkpointReached, _cancellationToken); - - private class Enumerator : IAsyncEnumerator { - private readonly IAsyncEnumerator<(SubscriptionConfirmation, Position? position, ResolvedEvent - resolvedEvent)> _inner; - - private readonly Func _checkpointReached; - private readonly CancellationToken _cancellationToken; - - public Enumerator(IAsyncEnumerator<(SubscriptionConfirmation, Position?, ResolvedEvent)> inner, - Func checkpointReached, CancellationToken cancellationToken) { - if (inner == null) { - throw new ArgumentNullException(nameof(inner)); - } - - if (checkpointReached == null) { - throw new ArgumentNullException(nameof(checkpointReached)); - } - - _inner = inner; - _checkpointReached = checkpointReached; - _cancellationToken = cancellationToken; - } - - public ValueTask DisposeAsync() => _inner.DisposeAsync(); - - public async ValueTask MoveNextAsync() { - ReadLoop: - if (_cancellationToken.IsCancellationRequested) { - return false; - } - - if (!await _inner.MoveNextAsync().ConfigureAwait(false)) { - return false; - } - - if (_cancellationToken.IsCancellationRequested) { - return false; - } - - if (!_inner.Current.position.HasValue) { - return true; - } - - await _checkpointReached(_inner.Current.position.Value).ConfigureAwait(false); - goto ReadLoop; - } - - public ResolvedEvent Current => _inner.Current.resolvedEvent; + _subscription.Dispose(); + _cts.Dispose(); } } } diff --git a/src/EventStore.Client.Streams/Streams/BatchAppendReq.cs b/src/EventStore.Client.Streams/Streams/BatchAppendReq.cs index 67f8153fd..f9e1146c0 100644 --- a/src/EventStore.Client.Streams/Streams/BatchAppendReq.cs +++ b/src/EventStore.Client.Streams/Streams/BatchAppendReq.cs @@ -9,7 +9,7 @@ public static Options Create(StreamIdentifier streamIdentifier, StreamRevision expectedStreamRevision, TimeSpan? timeoutAfter) => new() { StreamIdentifier = streamIdentifier, StreamPosition = expectedStreamRevision.ToUInt64(), - Deadline = Timestamp.FromDateTime(timeoutAfter.HasValue + Deadline21100 = Timestamp.FromDateTime(timeoutAfter.HasValue ? DateTime.UtcNow + timeoutAfter.Value : DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc)) }; @@ -24,7 +24,7 @@ public static Options Create(StreamIdentifier streamIdentifier, StreamState expe _ => ExpectedStreamPositionOneofCase.None }, expectedStreamPosition_ = new Google.Protobuf.WellKnownTypes.Empty(), - Deadline = Timestamp.FromDateTime(timeoutAfter.HasValue + Deadline21100 = Timestamp.FromDateTime(timeoutAfter.HasValue ? DateTime.UtcNow + timeoutAfter.Value : DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc)) }; diff --git a/src/EventStore.Client.Streams/SubscriptionConfirmation.cs b/src/EventStore.Client.Streams/SubscriptionConfirmation.cs deleted file mode 100644 index 368ce7f6b..000000000 --- a/src/EventStore.Client.Streams/SubscriptionConfirmation.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace EventStore.Client { - internal struct SubscriptionConfirmation : IEquatable { - public static readonly SubscriptionConfirmation None = default; - public string SubscriptionId { get; } - - public SubscriptionConfirmation(string subscriptionId) { - SubscriptionId = subscriptionId; - } - - public bool Equals(SubscriptionConfirmation other) => SubscriptionId == other.SubscriptionId; - public override bool Equals(object? obj) => obj is SubscriptionConfirmation other && Equals(other); - public override int GetHashCode() => SubscriptionId?.GetHashCode() ?? 0; - - public static bool operator ==(SubscriptionConfirmation left, SubscriptionConfirmation right) => - left.Equals(right); - - public static bool operator !=(SubscriptionConfirmation left, SubscriptionConfirmation right) => - !left.Equals(right); - - public override string ToString() => SubscriptionId; - } -} diff --git a/src/EventStore.Client.Streams/SubscriptionFilterOptions.cs b/src/EventStore.Client.Streams/SubscriptionFilterOptions.cs index 0858fc02d..38469d924 100644 --- a/src/EventStore.Client.Streams/SubscriptionFilterOptions.cs +++ b/src/EventStore.Client.Streams/SubscriptionFilterOptions.cs @@ -1,7 +1,3 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - namespace EventStore.Client { /// /// A class representing the options to use when filtering read operations. @@ -21,7 +17,8 @@ public class SubscriptionFilterOptions { /// A Task invoked and await when a checkpoint is reached. /// Set the checkpointInterval to define how often this method is called. /// - public Func CheckpointReached { get; } + [Obsolete] + public Func CheckpointReached { get; } = null!; /// /// @@ -33,8 +30,20 @@ public class SubscriptionFilterOptions { /// Set the checkpointInterval to define how often this method is called. /// /// - public SubscriptionFilterOptions(IEventFilter filter, uint checkpointInterval = 1, - Func? checkpointReached = null) { + [Obsolete] + public SubscriptionFilterOptions(IEventFilter filter, uint checkpointInterval, + Func? checkpointReached) + : this(filter, checkpointInterval) { + CheckpointReached = checkpointReached ?? ((_, __, ct) => Task.CompletedTask); + } + + /// + /// + /// + /// The to apply. + /// Sets how often the checkpointReached callback is called. + /// + public SubscriptionFilterOptions(IEventFilter filter, uint checkpointInterval = 1) { if (checkpointInterval == 0) { throw new ArgumentOutOfRangeException(nameof(checkpointInterval), checkpointInterval, $"{nameof(checkpointInterval)} must be greater than 0."); @@ -42,7 +51,6 @@ public SubscriptionFilterOptions(IEventFilter filter, uint checkpointInterval = Filter = filter; CheckpointInterval = checkpointInterval; - CheckpointReached = checkpointReached ?? ((_, __, ct) => Task.CompletedTask); } } } diff --git a/src/EventStore.Client/EventStore.Client.csproj b/src/EventStore.Client/EventStore.Client.csproj index 3ab3a3faf..ddf36c6af 100644 --- a/src/EventStore.Client/EventStore.Client.csproj +++ b/src/EventStore.Client/EventStore.Client.csproj @@ -24,9 +24,13 @@ - + + + + + diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs index 54fa95077..c94429ca3 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs @@ -10,17 +10,14 @@ public class Issue_1125 : IClassFixture { [Theory] [MemberData(nameof(TestCases))] public async Task persistent_subscription_delivers_all_events(int iteration) { - if (Environment.OSVersion.IsWindows()) { } - - const int eventCount = 250; + const int eventCount = 250; const int totalEvents = eventCount * 2; - var completed = new TaskCompletionSource(); - var hitCount = 0; + var hitCount = 0; var userCredentials = new UserCredentials("admin", "changeit"); - var streamName = $"stream_{iteration}"; + var streamName = $"stream_{iteration}"; var subscriptionName = $"subscription_{iteration}"; for (var i = 0; i < eventCount; i++) @@ -42,48 +39,46 @@ await _fixture.Client.CreateToStreamAsync( userCredentials: userCredentials ); - using (await _fixture.Client.SubscribeToStreamAsync( - streamName, - subscriptionName, - async (subscription, @event, retryCount, arg4) => { - int result; - if (retryCount == 0 || retryCount is null) { - result = Interlocked.Increment(ref hitCount); - - await subscription.Ack(@event); - - if (totalEvents == result) - completed.TrySetResult(true); - } - else { - // This is a retry - await subscription.Ack(@event); - } - }, - (s, dr, e) => { - if (e != null) - completed.TrySetException(e); - else - completed.TrySetException(new Exception($"{dr}")); - }, - userCredentials - ) - ) { + await using var subscription = + _fixture.Client.SubscribeToStream(streamName, subscriptionName, userCredentials: userCredentials); + + await Task.WhenAll(Subscribe(), Append()).WithTimeout(); + + Assert.Equal(totalEvents, hitCount); + + return; + + async Task Subscribe() { + await foreach (var message in subscription.Messages) { + if (message is not PersistentSubscriptionMessage.Event(var resolvedEvent, var retryCount)) { + continue; + } + + if (retryCount is 0 or null) { + var result = Interlocked.Increment(ref hitCount); + + await subscription.Ack(resolvedEvent); + + if (totalEvents == result) + return; + } else { + // This is a retry + await subscription.Ack(resolvedEvent); + } + } + } + + async Task Append() { for (var i = 0; i < eventCount; i++) await _fixture.StreamsClient.AppendToStreamAsync( streamName, StreamState.Any, - _fixture.CreateTestEvents() - ); - - await completed.Task.WithTimeout(TimeSpan.FromSeconds(30)); + _fixture.CreateTestEvents()); } - - Assert.Equal(totalEvents, hitCount); } public class Fixture : EventStoreClientFixture { protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; + protected override Task When() => Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs index d21d7061d..db2818abf 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs @@ -1,42 +1,31 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; public class connect_to_existing_with_max_one_client : IClassFixture { - const string Group = "maxoneclient"; + private const string Group = "maxoneclient"; - readonly Fixture _fixture; + private readonly Fixture _fixture; public connect_to_existing_with_max_one_client(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] - public async Task the_second_subscription_fails_to_connect() { - using var first = await _fixture.Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ).WithTimeout(); - + public async Task the_second_subscription_fails_to_connect2() { var ex = await Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ); - } - ).WithTimeout(); + () => Task.WhenAll(Subscribe().WithTimeout(), Subscribe().WithTimeout())); Assert.Equal(SystemStreams.AllStream, ex.StreamName); Assert.Equal(Group, ex.GroupName); + return; + + async Task Subscribe() { + await using var subscription = _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); + await subscription.Messages.AnyAsync(); + } } public class Fixture : EventStoreClientFixture { protected override Task Given() => - Client.CreateToAllAsync( - Group, - new(maxSubscriberCount: 1), - userCredentials: TestCredentials.Root - ); + Client.CreateToAllAsync(Group, new(maxSubscriberCount: 1), userCredentials: TestCredentials.Root); protected override Task When() => Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client_obsolete.cs new file mode 100644 index 000000000..200e12bb3 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client_obsolete.cs @@ -0,0 +1,43 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class connect_to_existing_with_max_one_client_obsolete : IClassFixture { + const string Group = "maxoneclient"; + + readonly Fixture _fixture; + + public connect_to_existing_with_max_one_client_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task the_second_subscription_fails_to_connect() { + using var first = await _fixture.Client.SubscribeToAllAsync( + Group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ).WithTimeout(); + + var ex = await Assert.ThrowsAsync( + async () => { + using var _ = await _fixture.Client.SubscribeToAllAsync( + Group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ); + } + ).WithTimeout(); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(Group, ex.GroupName); + } + + public class Fixture : EventStoreClientFixture { + protected override Task Given() => + Client.CreateToAllAsync( + Group, + new(maxSubscriberCount: 1), + userCredentials: TestCredentials.Root + ); + + protected override Task When() => Task.CompletedTask; + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs index 2cee251f5..e10ed9909 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs @@ -1,36 +1,23 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; -public class connect_to_existing_with_permissions - : IClassFixture { - const string Group = "connectwithpermissions"; - - readonly Fixture _fixture; +public class connect_to_existing_with_permissions : IClassFixture { + private const string Group = "connectwithpermissions"; + private readonly Fixture _fixture; public connect_to_existing_with_permissions(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] public async Task the_subscription_succeeds() { - var dropped = new TaskCompletionSource<(SubscriptionDroppedReason, Exception?)>(); - using var subscription = await _fixture.Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - (s, reason, ex) => dropped.TrySetResult((reason, ex)), - TestCredentials.Root - ).WithTimeout(); - - Assert.NotNull(subscription); + await using var subscription = + _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - await Assert.ThrowsAsync(() => dropped.Task.WithTimeout()); + Assert.True(await subscription.Messages + .FirstAsync().AsTask().WithTimeout() is PersistentSubscriptionMessage.SubscriptionConfirmation); } public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); + protected override Task Given() => Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); protected override Task When() => Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions_obsolete.cs new file mode 100644 index 000000000..719dc8b02 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions_obsolete.cs @@ -0,0 +1,37 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class connect_to_existing_with_permissions_obsolete + : IClassFixture { + const string Group = "connectwithpermissions"; + + readonly Fixture _fixture; + + public connect_to_existing_with_permissions_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task the_subscription_succeeds() { + var dropped = new TaskCompletionSource<(SubscriptionDroppedReason, Exception?)>(); + using var subscription = await _fixture.Client.SubscribeToAllAsync( + Group, + delegate { return Task.CompletedTask; }, + (s, reason, ex) => dropped.TrySetResult((reason, ex)), + TestCredentials.Root + ).WithTimeout(); + + Assert.NotNull(subscription); + + await Assert.ThrowsAsync(() => dropped.Task.WithTimeout()); + } + + public class Fixture : EventStoreClientFixture { + protected override Task Given() => + Client.CreateToAllAsync( + Group, + new(), + userCredentials: TestCredentials.Root + ); + + protected override Task When() => Task.CompletedTask; + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs index e28875425..47e36c620 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs @@ -1,65 +1,52 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; -public class connect_to_existing_with_start_from_beginning : IClassFixture { - const string Group = "startfrombeginning"; - - readonly Fixture _fixture; +public class connect_to_existing_with_start_from_beginning + : IClassFixture { + private const string Group = "startfrombeginning"; + private readonly Fixture _fixture; public connect_to_existing_with_start_from_beginning(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] public async Task the_subscription_gets_event_zero_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(_fixture.Events![0].Event.EventId, resolvedEvent.Event.EventId); + var resolvedEvent = await _fixture.Subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(_fixture.Events[0].Event.EventId, resolvedEvent.Event.EventId); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - - PersistentSubscription? _subscription; - - public Fixture() => _firstEventSource = new(); + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - public ResolvedEvent[]? Events { get; set; } - - public Task FirstEvent => _firstEventSource.Task; + public ResolvedEvent[] Events { get; private set; } = Array.Empty(); protected override async Task Given() { //append 10 events to random streams to make sure we have at least 10 events in the transaction file - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, new[] { @event }); + foreach (var @event in CreateTestEvents(10)) { + await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, + new[] { @event }); + } + + Events = await StreamsClient + .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + await Client.CreateToAllAsync(Group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); + } - Events = await StreamsClient.ReadAllAsync( - Direction.Forwards, - Position.Start, - 10, - userCredentials: TestCredentials.Root - ).ToArrayAsync(); + protected override Task When() { + Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start), - userCredentials: TestCredentials.Root - ); + return Task.CompletedTask; } - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning_obsolete.cs new file mode 100644 index 000000000..331930474 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning_obsolete.cs @@ -0,0 +1,66 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class connect_to_existing_with_start_from_beginning_obsolete : IClassFixture { + const string Group = "startfrombeginning"; + + readonly Fixture _fixture; + + public connect_to_existing_with_start_from_beginning_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task the_subscription_gets_event_zero_as_its_first_event() { + var resolvedEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(_fixture.Events![0].Event.EventId, resolvedEvent.Event.EventId); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstEventSource; + + PersistentSubscription? _subscription; + + public Fixture() => _firstEventSource = new(); + + public ResolvedEvent[]? Events { get; set; } + + public Task FirstEvent => _firstEventSource.Task; + + protected override async Task Given() { + //append 10 events to random streams to make sure we have at least 10 events in the transaction file + foreach (var @event in CreateTestEvents(10)) + await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, new[] { @event }); + + Events = await StreamsClient.ReadAllAsync( + Direction.Forwards, + Position.Start, + 10, + userCredentials: TestCredentials.Root + ).ToArrayAsync(); + + await Client.CreateToAllAsync( + Group, + new(startFrom: Position.Start), + userCredentials: TestCredentials.Root + ); + } + + protected override async Task When() => + _subscription = await Client.SubscribeToAllAsync( + Group, + async (subscription, e, r, ct) => { + _firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs index d25d8a9bd..17a28d1d6 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs @@ -1,62 +1,44 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; -public class connect_to_existing_with_start_from_not_set : IClassFixture { - const string Group = "startfromend1"; - readonly Fixture _fixture; +public class connect_to_existing_with_start_from_not_set + : IClassFixture { + private const string Group = "startfromend1"; + private readonly Fixture _fixture; public connect_to_existing_with_start_from_not_set(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] - public async Task the_subscription_gets_no_non_system_events() => - await Assert.ThrowsAsync(() => _fixture.FirstNonSystemEvent.WithTimeout()); + public async Task the_subscription_gets_no_non_system_events() { + await Assert.ThrowsAsync(() => _fixture.Subscription!.Messages + .OfType() + .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) + .AnyAsync() + .AsTask() + .WithTimeout(TimeSpan.FromMilliseconds(250))); + } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstNonSystemEventSource; - PersistentSubscription? _subscription; - - public Fixture() => _firstNonSystemEventSource = new(); - - public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } protected override async Task Given() { foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync( - "non-system-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { - @event - } - ); - - await Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); + await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any, + new[] { @event }); + + await Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); } - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { - await subscription.Ack(e); - return; - } - - _firstNonSystemEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstNonSystemEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + protected override Task When() { + Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); + return Task.CompletedTask; + } + + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_obsolete.cs new file mode 100644 index 000000000..2dce2c8dd --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_obsolete.cs @@ -0,0 +1,64 @@ + +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class connect_to_existing_with_start_from_not_set_obsolete : IClassFixture { + const string Group = "startfromend1"; + readonly Fixture _fixture; + + public connect_to_existing_with_start_from_not_set_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task the_subscription_gets_no_non_system_events() => + await Assert.ThrowsAsync(() => _fixture.FirstNonSystemEvent.WithTimeout()); + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstNonSystemEventSource; + PersistentSubscription? _subscription; + + public Fixture() => _firstNonSystemEventSource = new(); + + public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; + + protected override async Task Given() { + foreach (var @event in CreateTestEvents(10)) + await StreamsClient.AppendToStreamAsync( + "non-system-stream-" + Guid.NewGuid(), + StreamState.Any, + new[] { + @event + } + ); + + await Client.CreateToAllAsync( + Group, + new(), + userCredentials: TestCredentials.Root + ); + } + + protected override async Task When() => + _subscription = await Client.SubscribeToAllAsync( + Group, + async (subscription, e, r, ct) => { + if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { + await subscription.Ack(e); + return; + } + + _firstNonSystemEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstNonSystemEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs index c0f70ddb4..bbd8ef16e 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs @@ -2,70 +2,53 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; public class connect_to_existing_with_start_from_not_set_then_event_written : IClassFixture { - const string Group = "startfromnotset2"; + private const string Group = "startfromnotset2"; - readonly Fixture _fixture; + private readonly Fixture _fixture; - public - connect_to_existing_with_start_from_not_set_then_event_written(Fixture fixture) => - _fixture = fixture; + public connect_to_existing_with_start_from_not_set_then_event_written(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event() { - var resolvedEvent = await _fixture.FirstNonSystemEvent.WithTimeout(); - Assert.Equal(_fixture.ExpectedEvent!.EventId, resolvedEvent.Event.EventId); + var resolvedEvent = await _fixture.Subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .Where(resolvedEvent => !SystemStreams.IsSystemStream(resolvedEvent.OriginalStreamId)) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(_fixture.ExpectedEvent.EventId, resolvedEvent.Event.EventId); Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstNonSystemEventSource; - - public readonly EventData? ExpectedEvent; - public readonly string ExpectedStreamId; + public readonly EventData ExpectedEvent; + public readonly string ExpectedStreamId; - PersistentSubscription? _subscription; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _firstNonSystemEventSource = new(); - ExpectedEvent = CreateTestEvents(1).First(); - ExpectedStreamId = Guid.NewGuid().ToString(); + ExpectedEvent = CreateTestEvents(1).First(); + ExpectedStreamId = Guid.NewGuid().ToString(); } - public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync( - "non-system-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { @event } - ); + foreach (var @event in CreateTestEvents(10)) { + await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any, + new[] { @event }); + } await Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { - await subscription.Ack(e); - return; - } - - _firstNonSystemEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstNonSystemEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); + Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); } - protected override async Task When() => await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent! }); + protected override async Task When() => + await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent }); + + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs new file mode 100644 index 000000000..95dc8edf6 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs @@ -0,0 +1,72 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class connect_to_existing_with_start_from_not_set_then_event_written_obsolete + : IClassFixture { + const string Group = "startfromnotset2"; + + readonly Fixture _fixture; + + public + connect_to_existing_with_start_from_not_set_then_event_written_obsolete(Fixture fixture) => + _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event2() { + var resolvedEvent = await _fixture.FirstNonSystemEvent.WithTimeout(); + Assert.Equal(_fixture.ExpectedEvent!.EventId, resolvedEvent.Event.EventId); + Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstNonSystemEventSource; + + public readonly EventData? ExpectedEvent; + public readonly string ExpectedStreamId; + + PersistentSubscription? _subscription; + + public Fixture() { + _firstNonSystemEventSource = new(); + ExpectedEvent = CreateTestEvents(1).First(); + ExpectedStreamId = Guid.NewGuid().ToString(); + } + + public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; + + protected override async Task Given() { + foreach (var @event in CreateTestEvents(10)) + await StreamsClient.AppendToStreamAsync( + "non-system-stream-" + Guid.NewGuid(), + StreamState.Any, + new[] { @event } + ); + + await Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); + _subscription = await Client.SubscribeToAllAsync( + Group, + async (subscription, e, r, ct) => { + if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { + await subscription.Ack(e); + return; + } + + _firstNonSystemEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstNonSystemEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + } + + protected override async Task When() => await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent! }); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs index bc1632630..e7f3c9913 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs @@ -1,62 +1,46 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; -public class connect_to_existing_with_start_from_set_to_end_position : IClassFixture { - const string Group = "startfromend1"; +public class connect_to_existing_with_start_from_set_to_end_position + : IClassFixture { + private const string Group = "startfromend1"; - readonly Fixture _fixture; + private readonly Fixture _fixture; public connect_to_existing_with_start_from_set_to_end_position(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] - public async Task the_subscription_gets_no_non_system_events() => - await Assert.ThrowsAsync(() => _fixture.FirstNonSystemEvent.WithTimeout()); + public async Task the_subscription_gets_no_non_system_events() { + await Assert.ThrowsAsync(() => _fixture.Subscription!.Messages + .OfType() + .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) + .AnyAsync() + .AsTask() + .WithTimeout(TimeSpan.FromMilliseconds(250))); + } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstNonSystemEventSource; - - PersistentSubscription? _subscription; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - public Fixture() => _firstNonSystemEventSource = new(); + protected override async Task Given() { + foreach (var @event in CreateTestEvents(10)) { + await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any, + new[] { @event }); + } - public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; + await Client.CreateToAllAsync(Group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); + } - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync( - "non-system-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { @event } - ); - - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.End), - userCredentials: TestCredentials.Root - ); + protected override Task When() { + Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); + return Task.CompletedTask; } - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { - await subscription.Ack(e); - return; - } - - _firstNonSystemEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstNonSystemEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs new file mode 100644 index 000000000..5b82b063d --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs @@ -0,0 +1,63 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class connect_to_existing_with_start_from_set_to_end_position_obsolete: IClassFixture { + const string Group = "startfromend1"; + + readonly Fixture _fixture; + + public connect_to_existing_with_start_from_set_to_end_position_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task the_subscription_gets_no_non_system_events() => + await Assert.ThrowsAsync(() => _fixture.FirstNonSystemEvent.WithTimeout()); + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstNonSystemEventSource; + + PersistentSubscription? _subscription; + + public Fixture() => _firstNonSystemEventSource = new(); + + public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; + + protected override async Task Given() { + foreach (var @event in CreateTestEvents(10)) + await StreamsClient.AppendToStreamAsync( + "non-system-stream-" + Guid.NewGuid(), + StreamState.Any, + new[] { @event } + ); + + await Client.CreateToAllAsync( + Group, + new(startFrom: Position.End), + userCredentials: TestCredentials.Root + ); + } + + protected override async Task When() => + _subscription = await Client.SubscribeToAllAsync( + Group, + async (subscription, e, r, ct) => { + if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { + await subscription.Ack(e); + return; + } + + _firstNonSystemEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstNonSystemEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs index 9fee746b8..835c8538c 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs @@ -3,66 +3,54 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; public class connect_to_existing_with_start_from_set_to_end_position_then_event_written : IClassFixture { - const string Group = "startfromnotset2"; + private const string Group = "startfromnotset2"; + private readonly Fixture _fixture; - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_end_position_then_event_written(Fixture fixture) => _fixture = fixture; + public connect_to_existing_with_start_from_set_to_end_position_then_event_written(Fixture fixture) => + _fixture = fixture; [SupportsPSToAll.Fact] public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event() { - var resolvedEvent = await _fixture.FirstNonSystemEvent.WithTimeout(); + var resolvedEvent = await _fixture.Subscription!.Messages + .OfType() + .Select(e => e.ResolvedEvent) + .Where(resolvedEvent => !SystemStreams.IsSystemStream(resolvedEvent.OriginalStreamId)) + .FirstAsync() + .AsTask() + .WithTimeout(); Assert.Equal(_fixture.ExpectedEvent.EventId, resolvedEvent.Event.EventId); Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstNonSystemEventSource; - public readonly EventData ExpectedEvent; - public readonly string ExpectedStreamId; - PersistentSubscription? _subscription; + public readonly EventData ExpectedEvent; + public readonly string ExpectedStreamId; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _firstNonSystemEventSource = new(); - ExpectedEvent = CreateTestEvents(1).First(); - ExpectedStreamId = Guid.NewGuid().ToString(); + ExpectedEvent = CreateTestEvents(1).First(); + ExpectedStreamId = Guid.NewGuid().ToString(); } - public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync( - "non-system-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { @event } - ); + foreach (var @event in CreateTestEvents(10)) { + await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any, + new[] { @event }); + } await Client.CreateToAllAsync(Group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { - await subscription.Ack(e); - return; - } - - _firstNonSystemEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstNonSystemEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); + Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); } - protected override async Task When() => await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent }); + protected override async Task When() => + await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent }); + + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs new file mode 100644 index 000000000..4f9dea90e --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs @@ -0,0 +1,69 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class + connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete + : IClassFixture { + const string Group = "startfromnotset2"; + + readonly Fixture _fixture; + + public connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event() { + var resolvedEvent = await _fixture.FirstNonSystemEvent.WithTimeout(); + Assert.Equal(_fixture.ExpectedEvent.EventId, resolvedEvent.Event.EventId); + Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstNonSystemEventSource; + public readonly EventData ExpectedEvent; + public readonly string ExpectedStreamId; + PersistentSubscription? _subscription; + + public Fixture() { + _firstNonSystemEventSource = new(); + ExpectedEvent = CreateTestEvents(1).First(); + ExpectedStreamId = Guid.NewGuid().ToString(); + } + + public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; + + protected override async Task Given() { + foreach (var @event in CreateTestEvents(10)) + await StreamsClient.AppendToStreamAsync( + "non-system-stream-" + Guid.NewGuid(), + StreamState.Any, + new[] { @event } + ); + + await Client.CreateToAllAsync(Group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); + _subscription = await Client.SubscribeToAllAsync( + Group, + async (subscription, e, r, ct) => { + if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { + await subscription.Ack(e); + return; + } + + _firstNonSystemEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstNonSystemEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + } + + protected override async Task When() => await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent }); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs index 5c0463a02..b45330a9e 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs @@ -2,47 +2,47 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; public class connect_to_existing_with_start_from_set_to_invalid_middle_position : IClassFixture { - const string Group = "startfrominvalid1"; - readonly Fixture _fixture; + private const string Group = "startfrominvalid1"; + private readonly Fixture _fixture; public connect_to_existing_with_start_from_set_to_invalid_middle_position(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] public async Task the_subscription_is_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - Assert.IsType(exception); + var ex = await Assert.ThrowsAsync(async () => + await _fixture.Enumerator!.MoveNextAsync()); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(Group, ex.GroupName); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped; - - PersistentSubscription? _subscription; - - public Fixture() => _dropped = new(); - - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } + public IAsyncEnumerator? Enumerator { get; private set; } protected override async Task Given() { var invalidPosition = new Position(1L, 1L); - await Client.CreateToAllAsync( - Group, - new(startFrom: invalidPosition), - userCredentials: TestCredentials.Root - ); + await Client.CreateToAllAsync(Group, new(startFrom: invalidPosition), + userCredentials: TestCredentials.Root); } - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => await subscription.Ack(e), - (subscription, reason, ex) => { _dropped.TrySetResult((reason, ex)); }, - TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + protected override async Task When() { + Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); + Enumerator = Subscription.Messages.GetAsyncEnumerator(); + + await Enumerator.MoveNextAsync(); + } + + public override async Task DisposeAsync() { + if (Enumerator is not null) { + await Enumerator.DisposeAsync(); + } + + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs new file mode 100644 index 000000000..639164294 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs @@ -0,0 +1,49 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete + : IClassFixture { + const string Group = "startfrominvalid1"; + readonly Fixture _fixture; + + public connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task the_subscription_is_dropped() { + var (reason, exception) = await _fixture.Dropped.WithTimeout(); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + Assert.IsType(exception); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped; + + PersistentSubscription? _subscription; + + public Fixture() => _dropped = new(); + + public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task; + + protected override async Task Given() { + var invalidPosition = new Position(1L, 1L); + await Client.CreateToAllAsync( + Group, + new(startFrom: invalidPosition), + userCredentials: TestCredentials.Root + ); + } + + protected override async Task When() => + _subscription = await Client.SubscribeToAllAsync( + Group, + async (subscription, e, r, ct) => await subscription.Ack(e), + (subscription, reason, ex) => { _dropped.TrySetResult((reason, ex)); }, + TestCredentials.Root + ); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs index 9469af0af..daaf838a9 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs @@ -2,63 +2,50 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; public class connect_to_existing_with_start_from_set_to_valid_middle_position : IClassFixture { - const string Group = "startfromvalid"; + private const string Group = "startfromvalid"; - readonly Fixture _fixture; + private readonly Fixture _fixture; public connect_to_existing_with_start_from_set_to_valid_middle_position(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] public async Task the_subscription_gets_the_event_at_the_specified_start_position_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); + var resolvedEvent = await _fixture.Subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstAsync() + .AsTask() + .WithTimeout(); Assert.Equal(_fixture.ExpectedEvent.OriginalPosition, resolvedEvent.Event.Position); Assert.Equal(_fixture.ExpectedEvent.Event.EventId, resolvedEvent.Event.EventId); Assert.Equal(_fixture.ExpectedEvent.Event.EventStreamId, resolvedEvent.Event.EventStreamId); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - PersistentSubscription? _subscription; - - public Fixture() => _firstEventSource = new(); - - public Task FirstEvent => _firstEventSource.Task; - public ResolvedEvent ExpectedEvent { get; private set; } + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } + public ResolvedEvent ExpectedEvent { get; private set; } protected override async Task Given() { - var events = await StreamsClient.ReadAllAsync( - Direction.Forwards, - Position.Start, - 10, - userCredentials: TestCredentials.Root - ).ToArrayAsync(); + var events = await StreamsClient + .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root) + .ToArrayAsync(); ExpectedEvent = events[events.Length / 2]; //just a random event in the middle of the results - await Client.CreateToAllAsync( - Group, - new(startFrom: ExpectedEvent.OriginalPosition), - userCredentials: TestCredentials.Root - ); + await Client.CreateToAllAsync(Group, new(startFrom: ExpectedEvent.OriginalPosition), + userCredentials: TestCredentials.Root); + } + + protected override Task When() { + Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); + return Task.CompletedTask; } - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs new file mode 100644 index 000000000..7fc7b07c8 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs @@ -0,0 +1,65 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete + : IClassFixture { + const string Group = "startfromvalid"; + + readonly Fixture _fixture; + + public connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task the_subscription_gets_the_event_at_the_specified_start_position_as_its_first_event() { + var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); + Assert.Equal(_fixture.ExpectedEvent.OriginalPosition, resolvedEvent.Event.Position); + Assert.Equal(_fixture.ExpectedEvent.Event.EventId, resolvedEvent.Event.EventId); + Assert.Equal(_fixture.ExpectedEvent.Event.EventStreamId, resolvedEvent.Event.EventStreamId); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstEventSource; + PersistentSubscription? _subscription; + + public Fixture() => _firstEventSource = new(); + + public Task FirstEvent => _firstEventSource.Task; + public ResolvedEvent ExpectedEvent { get; private set; } + + protected override async Task Given() { + var events = await StreamsClient.ReadAllAsync( + Direction.Forwards, + Position.Start, + 10, + userCredentials: TestCredentials.Root + ).ToArrayAsync(); + + ExpectedEvent = events[events.Length / 2]; //just a random event in the middle of the results + + await Client.CreateToAllAsync( + Group, + new(startFrom: ExpectedEvent.OriginalPosition), + userCredentials: TestCredentials.Root + ); + } + + protected override async Task When() => + _subscription = await Client.SubscribeToAllAsync( + Group, + async (subscription, e, r, ct) => { + _firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs index 8f18f498f..865281ec0 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs @@ -1,31 +1,23 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; -public class connect_to_existing_without_permissions - : IClassFixture { - readonly Fixture _fixture; +public class connect_to_existing_without_permissions : IClassFixture { + private readonly Fixture _fixture; public connect_to_existing_without_permissions(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] public Task throws_access_denied() => Assert.ThrowsAsync( async () => { - using var _ = await _fixture.Client.SubscribeToAllAsync( - "agroupname55", - delegate { return Task.CompletedTask; } - ); - } - ).WithTimeout(); + await using var subscription = _fixture.Client.SubscribeToAll("agroupname55"); + await subscription.AnyAsync().AsTask().WithTimeout(); + }); public class Fixture : EventStoreClientFixture { public Fixture() : base(noDefaultCredentials: true) { } protected override Task Given() => - Client.CreateToAllAsync( - "agroupname55", - new(), - userCredentials: TestCredentials.Root - ); + Client.CreateToAllAsync("agroupname55", new(), userCredentials: TestCredentials.Root); protected override Task When() => Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions_obsolete.cs new file mode 100644 index 000000000..82ee362b8 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions_obsolete.cs @@ -0,0 +1,32 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class connect_to_existing_without_permissions_obsolete + : IClassFixture { + readonly Fixture _fixture; + public connect_to_existing_without_permissions_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public Task throws_access_denied() => + Assert.ThrowsAsync( + async () => { + using var _ = await _fixture.Client.SubscribeToAllAsync( + "agroupname55", + delegate { return Task.CompletedTask; } + ); + } + ).WithTimeout(); + + public class Fixture : EventStoreClientFixture { + public Fixture() : base(noDefaultCredentials: true) { } + + protected override Task Given() => + Client.CreateToAllAsync( + "agroupname55", + new(), + userCredentials: TestCredentials.Root + ); + + protected override Task When() => Task.CompletedTask; + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs index 7499264d9..56eccd0fe 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs @@ -1,32 +1,25 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; -public class connect_to_existing_without_read_all_permissions +public class connect_to_existing_without_read_all_permissions : IClassFixture { - readonly Fixture _fixture; + private readonly Fixture _fixture; public connect_to_existing_without_read_all_permissions(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] public Task throws_access_denied() => Assert.ThrowsAsync( async () => { - using var _ = await _fixture.Client.SubscribeToAllAsync( - "agroupname55", - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.TestUser1 - ); - } - ).WithTimeout(); + await using var subscription = + _fixture.Client.SubscribeToAll("agroupname55", userCredentials: TestCredentials.TestUser1); + await subscription.Messages.AnyAsync().AsTask().WithTimeout(); + }); public class Fixture : EventStoreClientFixture { public Fixture() : base(noDefaultCredentials: true) { } protected override Task Given() => - Client.CreateToAllAsync( - "agroupname55", - new(), - userCredentials: TestCredentials.Root - ); + Client.CreateToAllAsync("agroupname55", new(), userCredentials: TestCredentials.Root); protected override Task When() => Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions_obsolete.cs new file mode 100644 index 000000000..0ffe8daf4 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions_obsolete.cs @@ -0,0 +1,33 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class connect_to_existing_without_read_all_permissions_obsolete + : IClassFixture { + readonly Fixture _fixture; + public connect_to_existing_without_read_all_permissions_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public Task throws_access_denied() => + Assert.ThrowsAsync( + async () => { + using var _ = await _fixture.Client.SubscribeToAllAsync( + "agroupname55", + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.TestUser1 + ); + } + ).WithTimeout(); + + public class Fixture : EventStoreClientFixture { + public Fixture() : base(noDefaultCredentials: true) { } + + protected override Task Given() => + Client.CreateToAllAsync( + "agroupname55", + new(), + userCredentials: TestCredentials.Root + ); + + protected override Task When() => Task.CompletedTask; + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs index 9b452a46f..40a541040 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs @@ -2,30 +2,23 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; public class connect_to_non_existing_with_permissions : IClassFixture { - const string Group = "foo"; + private const string Group = "foo"; - readonly Fixture _fixture; + private readonly Fixture _fixture; public connect_to_non_existing_with_permissions(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] public async Task throws_persistent_subscription_not_found() { - var ex = await Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ); - } - ).WithTimeout(); + await using var subscription = _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); + Assert.True(await subscription.Messages.OfType().AnyAsync() + .AsTask() + .WithTimeout()); } public class Fixture : EventStoreClientFixture { protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; + protected override Task When() => Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions_obsolete.cs new file mode 100644 index 000000000..06f9a6d68 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions_obsolete.cs @@ -0,0 +1,32 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class connect_to_non_existing_with_permissions_obsolete + : IClassFixture { + const string Group = "foo"; + + readonly Fixture _fixture; + + public connect_to_non_existing_with_permissions_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task throws_persistent_subscription_not_found() { + var ex = await Assert.ThrowsAsync( + async () => { + using var _ = await _fixture.Client.SubscribeToAllAsync( + Group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ); + } + ).WithTimeout(); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(Group, ex.GroupName); + } + + public class Fixture : EventStoreClientFixture { + protected override Task Given() => Task.CompletedTask; + protected override Task When() => Task.CompletedTask; + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs index 7fc26350e..ea4236278 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs @@ -1,58 +1,49 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; -public class connect_with_retries - : IClassFixture { - const string Group = "retries"; - readonly Fixture _fixture; +public class connect_with_retries : IClassFixture { + private const string Group = "retries"; + private readonly Fixture _fixture; public connect_with_retries(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] - public async Task events_are_retried_until_success() => Assert.Equal(5, await _fixture.RetryCount.WithTimeout()); + public async Task events_are_retried_until_success() { + var retryCount = await _fixture.Subscription!.Messages.OfType() + .SelectAwait(async e => { + if (e.RetryCount > 4) { + await _fixture.Subscription.Ack(e.ResolvedEvent); + } else { + await _fixture.Subscription.Nack(PersistentSubscriptionNakEventAction.Retry, + "Not yet tried enough times", e.ResolvedEvent); + } + + return e.RetryCount; + }) + .Where(retryCount => retryCount > 4) + .FirstOrDefaultAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(5, retryCount); + } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _retryCountSource; - PersistentSubscription? _subscription; - - public Fixture() => _retryCountSource = new(); - - public Task RetryCount => _retryCountSource.Task; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } protected override async Task Given() { - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (r > 4) { - _retryCountSource.TrySetResult(r.Value); - await subscription.Ack(e.Event.EventId); - } - else { - await subscription.Nack( - PersistentSubscriptionNakEventAction.Retry, - "Not yet tried enough times", - e - ); - } - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _retryCountSource.TrySetException(ex!); - }, - TestCredentials.Root - ); + await Client.CreateToAllAsync(Group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); + + Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); } protected override Task When() => Task.CompletedTask; - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries_obsolete.cs new file mode 100644 index 000000000..0eff7957e --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries_obsolete.cs @@ -0,0 +1,59 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class connect_with_retries_obsolete + : IClassFixture { + const string Group = "retries"; + readonly Fixture _fixture; + + public connect_with_retries_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task events_are_retried_until_success() => Assert.Equal(5, await _fixture.RetryCount.WithTimeout()); + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _retryCountSource; + PersistentSubscription? _subscription; + + public Fixture() => _retryCountSource = new(); + + public Task RetryCount => _retryCountSource.Task; + + protected override async Task Given() { + await Client.CreateToAllAsync( + Group, + new(startFrom: Position.Start), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToAllAsync( + Group, + async (subscription, e, r, ct) => { + if (r > 4) { + _retryCountSource.TrySetResult(r.Value); + await subscription.Ack(e.Event.EventId); + } + else { + await subscription.Nack( + PersistentSubscriptionNakEventAction.Retry, + "Not yet tried enough times", + e + ); + } + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _retryCountSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + } + + protected override Task When() => Task.CompletedTask; + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs index 4403087e6..4db83e251 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs @@ -1,67 +1,23 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; -public class deleting_existing_with_subscriber - : IClassFixture { - readonly Fixture _fixture; +public class deleting_existing_with_subscriber : IClassFixture { + public const string Group = "groupname123"; + private readonly Fixture _fixture; public deleting_existing_with_subscriber(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] - public async Task the_subscription_is_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - -#if NET - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal("groupname123", ex.GroupName); -#endif - } - - [Fact(Skip = "Isn't this how it should work?")] public async Task the_subscription_is_dropped_with_not_found() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal("groupname123", ex.GroupName); + await using var subscription = _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); + + Assert.True(await subscription.Messages.OfType().AnyAsync() + .AsTask() + .WithTimeout()); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped; - PersistentSubscription? _subscription; - - public Fixture() => _dropped = new(); - - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task; - - protected override async Task Given() { - await Client.CreateToAllAsync( - "groupname123", - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - "groupname123", - async (s, e, i, ct) => await s.Ack(e), - (s, r, e) => _dropped.TrySetResult((r, e)), - TestCredentials.Root - ); - - // todo: investigate why this test is flaky without this delay - await Task.Delay(500); - } - - protected override Task When() => - Client.DeleteToAllAsync( - "groupname123", - userCredentials: TestCredentials.Root - ); + protected override Task Given() => Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } + protected override Task When() => Client.DeleteToAllAsync(Group, userCredentials: TestCredentials.Root); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber_obsolete.cs new file mode 100644 index 000000000..556224f23 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber_obsolete.cs @@ -0,0 +1,66 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class deleting_existing_with_subscriber_obsolete + : IClassFixture { + readonly Fixture _fixture; + + public deleting_existing_with_subscriber_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task the_subscription_is_dropped() { + var (reason, exception) = await _fixture.Dropped.WithTimeout(); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal("groupname123", ex.GroupName); + } + + [Fact(Skip = "Isn't this how it should work?")] + public async Task the_subscription_is_dropped_with_not_found() { + var (reason, exception) = await _fixture.Dropped.WithTimeout(); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal("groupname123", ex.GroupName); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped; + PersistentSubscription? _subscription; + + public Fixture() => _dropped = new(); + + public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task; + + protected override async Task Given() { + await Client.CreateToAllAsync( + "groupname123", + new(), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToAllAsync( + "groupname123", + async (s, e, i, ct) => await s.Ack(e), + (s, r, e) => _dropped.TrySetResult((r, e)), + TestCredentials.Root + ); + + // todo: investigate why this test is flaky without this delay + await Task.Delay(500); + } + + protected override Task When() => + Client.DeleteToAllAsync( + "groupname123", + userCredentials: TestCredentials.Root + ); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs index ca3afffd6..ab748a5d5 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs @@ -1,9 +1,9 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; public class get_info : IClassFixture { - const string GroupName = nameof(get_info); + private const string GroupName = nameof(get_info); - static readonly PersistentSubscriptionSettings Settings = new( + private static readonly PersistentSubscriptionSettings Settings = new( resolveLinkTos: true, startFrom: Position.Start, extraStatistics: true, @@ -16,19 +16,18 @@ public class get_info : IClassFixture { checkPointLowerBound: 1, checkPointUpperBound: 1, maxSubscriberCount: 500, - consumerStrategyName: SystemConsumerStrategies.Pinned - ); + consumerStrategyName: SystemConsumerStrategies.Pinned); - readonly Fixture _fixture; + private readonly Fixture _fixture; public get_info(Fixture fixture) => _fixture = fixture; [Fact] public async Task throws_when_not_supported() { - if (SupportsPSToAll.No) - await Assert.ThrowsAsync( - async () => { await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root); } - ); + if (SupportsPSToAll.No) { + await Assert.ThrowsAsync(async () => + await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root)); + } } [SupportsPSToAll.Fact] @@ -100,56 +99,45 @@ public async Task returns_expected_result() { [SupportsPSToAll.Fact] public async Task throws_with_non_existing_subscription() => await Assert.ThrowsAsync( - async () => { - await _fixture.Client.GetInfoToAllAsync( - "NonExisting", - userCredentials: TestCredentials.Root - ); - } - ); + async () => await _fixture.Client.GetInfoToAllAsync("NonExisting", userCredentials: TestCredentials.Root)); [SupportsPSToAll.Fact] public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync(async () => { await _fixture.Client.GetInfoToAllAsync("NonExisting"); }); + await Assert.ThrowsAsync(async () => + await _fixture.Client.GetInfoToAllAsync("NonExisting")); [SupportsPSToAll.Fact] public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync( - async () => { - await _fixture.Client.GetInfoToAllAsync( - "NonExisting", - userCredentials: TestCredentials.TestBadUser - ); - } - ); + await Assert.ThrowsAsync(async () => + await _fixture.Client.GetInfoToAllAsync("NonExisting", userCredentials: TestCredentials.TestBadUser)); [SupportsPSToAll.Fact] public async Task returns_result_with_normal_user_credentials() { - var result = await _fixture.Client.GetInfoToAllAsync( - GroupName, - userCredentials: TestCredentials.TestUser1 - ); + var result = await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.TestUser1); Assert.Equal("$all", result.EventSource); } - void AssertKeyAndValue(IDictionary items, string key) { + private void AssertKeyAndValue(IDictionary items, string key) { Assert.True(items.ContainsKey(key)); Assert.True(items[key] > 0); } public class Fixture : EventStoreClientFixture { + private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; + private IAsyncEnumerator? _enumerator; public Fixture() : base(noDefaultCredentials: true) { } protected override async Task Given() { if (SupportsPSToAll.No) return; - await Client.CreateToAllAsync( - GroupName, - get_info.Settings, - userCredentials: TestCredentials.Root - ); + await Client.CreateToAllAsync(GroupName, get_info.Settings, userCredentials: TestCredentials.Root); + + foreach (var eventData in CreateTestEvents(20)) { + await StreamsClient.AppendToStreamAsync($"test-{Guid.NewGuid():n}", StreamState.NoStream, + new[] { eventData }, userCredentials: TestCredentials.Root); + } } protected override async Task When() { @@ -157,26 +145,37 @@ protected override async Task When() { return; var counter = 0; - var tcs = new TaskCompletionSource(); - - await Client.SubscribeToAllAsync( - GroupName, - (s, e, r, ct) => { - counter++; - - switch (counter) { - case 1: s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e); - break; - case > 10: - tcs.TrySetResult(); - break; - } - return Task.CompletedTask; - }, - userCredentials: TestCredentials.Root - ); - - await tcs.Task; + + _subscription = Client.SubscribeToAll(GroupName, userCredentials: TestCredentials.Root); + _enumerator = _subscription.Messages.GetAsyncEnumerator(); + + while (await _enumerator.MoveNextAsync()) { + if (_enumerator.Current is not PersistentSubscriptionMessage.Event (var resolvedEvent, _)) { + continue; + } + + counter++; + + if (counter == 1) { + await _subscription.Nack(PersistentSubscriptionNakEventAction.Park, "Test", resolvedEvent); + } + + if (counter > 10) { + return; + } + } + } + + public override async Task DisposeAsync() { + if (_enumerator is not null) { + await _enumerator.DisposeAsync(); + } + + if (_subscription is not null) { + await _subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } } diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info_obsolete.cs new file mode 100644 index 000000000..d000304bb --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info_obsolete.cs @@ -0,0 +1,183 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class get_info_obsolete : IClassFixture { + const string GroupName = nameof(get_info_obsolete); + + static readonly PersistentSubscriptionSettings Settings = new( + resolveLinkTos: true, + startFrom: Position.Start, + extraStatistics: true, + messageTimeout: TimeSpan.FromSeconds(9), + maxRetryCount: 11, + liveBufferSize: 303, + readBatchSize: 30, + historyBufferSize: 909, + checkPointAfter: TimeSpan.FromSeconds(1), + checkPointLowerBound: 1, + checkPointUpperBound: 1, + maxSubscriberCount: 500, + consumerStrategyName: SystemConsumerStrategies.Pinned + ); + + readonly Fixture _fixture; + + public get_info_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task throws_when_not_supported() { + if (SupportsPSToAll.No) + await Assert.ThrowsAsync( + async () => { await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root); } + ); + } + + [SupportsPSToAll.Fact] + public async Task returns_expected_result() { + var result = await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root); + + Assert.Equal("$all", result.EventSource); + Assert.Equal(GroupName, result.GroupName); + Assert.Equal("Live", result.Status); + + Assert.NotNull(Settings.StartFrom); + Assert.True(result.Stats.TotalItems > 0); + Assert.True(result.Stats.OutstandingMessagesCount > 0); + Assert.True(result.Stats.AveragePerSecond >= 0); + Assert.True(result.Stats.ParkedMessageCount > 0); + Assert.True(result.Stats.AveragePerSecond >= 0); + Assert.True(result.Stats.CountSinceLastMeasurement >= 0); + Assert.True(result.Stats.TotalInFlightMessages >= 0); + Assert.NotNull(result.Stats.LastKnownEventPosition); + Assert.NotNull(result.Stats.LastCheckpointedEventPosition); + Assert.True(result.Stats.LiveBufferCount >= 0); + + Assert.NotNull(result.Connections); + Assert.NotEmpty(result.Connections); + + var connection = result.Connections.First(); + Assert.NotNull(connection.From); + Assert.Equal(TestCredentials.Root.Username, connection.Username); + Assert.NotEmpty(connection.ConnectionName); + Assert.True(connection.AverageItemsPerSecond >= 0); + Assert.True(connection.TotalItems > 0); + Assert.True(connection.CountSinceLastMeasurement >= 0); + Assert.True(connection.AvailableSlots >= 0); + Assert.True(connection.InFlightMessages >= 0); + Assert.NotNull(connection.ExtraStatistics); + Assert.NotEmpty(connection.ExtraStatistics); + + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Highest); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Mean); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Median); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Fastest); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile1); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile2); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile3); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile4); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile5); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyPercent); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyFivePercent); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePercent); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointFivePercent); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointNinePercent); + + Assert.NotNull(result.Settings); + Assert.Equal(Settings.StartFrom, result.Settings!.StartFrom); + Assert.Equal(Settings.ResolveLinkTos, result.Settings!.ResolveLinkTos); + Assert.Equal(Settings.ExtraStatistics, result.Settings!.ExtraStatistics); + Assert.Equal(Settings.MessageTimeout, result.Settings!.MessageTimeout); + Assert.Equal(Settings.MaxRetryCount, result.Settings!.MaxRetryCount); + Assert.Equal(Settings.LiveBufferSize, result.Settings!.LiveBufferSize); + Assert.Equal(Settings.ReadBatchSize, result.Settings!.ReadBatchSize); + Assert.Equal(Settings.HistoryBufferSize, result.Settings!.HistoryBufferSize); + Assert.Equal(Settings.CheckPointAfter, result.Settings!.CheckPointAfter); + Assert.Equal(Settings.CheckPointLowerBound, result.Settings!.CheckPointLowerBound); + Assert.Equal(Settings.CheckPointUpperBound, result.Settings!.CheckPointUpperBound); + Assert.Equal(Settings.MaxSubscriberCount, result.Settings!.MaxSubscriberCount); + Assert.Equal(Settings.ConsumerStrategyName, result.Settings!.ConsumerStrategyName); + } + + [SupportsPSToAll.Fact] + public async Task throws_with_non_existing_subscription() => + await Assert.ThrowsAsync( + async () => { + await _fixture.Client.GetInfoToAllAsync( + "NonExisting", + userCredentials: TestCredentials.Root + ); + } + ); + + [SupportsPSToAll.Fact] + public async Task throws_with_no_credentials() => + await Assert.ThrowsAsync(async () => { await _fixture.Client.GetInfoToAllAsync("NonExisting"); }); + + [SupportsPSToAll.Fact] + public async Task throws_with_non_existing_user() => + await Assert.ThrowsAsync( + async () => { + await _fixture.Client.GetInfoToAllAsync( + "NonExisting", + userCredentials: TestCredentials.TestBadUser + ); + } + ); + + [SupportsPSToAll.Fact] + public async Task returns_result_with_normal_user_credentials() { + var result = await _fixture.Client.GetInfoToAllAsync( + GroupName, + userCredentials: TestCredentials.TestUser1 + ); + + Assert.Equal("$all", result.EventSource); + } + + void AssertKeyAndValue(IDictionary items, string key) { + Assert.True(items.ContainsKey(key)); + Assert.True(items[key] > 0); + } + + public class Fixture : EventStoreClientFixture { + public Fixture() : base(noDefaultCredentials: true) { } + + protected override async Task Given() { + if (SupportsPSToAll.No) + return; + + await Client.CreateToAllAsync( + GroupName, + get_info_obsolete.Settings, + userCredentials: TestCredentials.Root + ); + } + + protected override async Task When() { + if (SupportsPSToAll.No) + return; + + var counter = 0; + var tcs = new TaskCompletionSource(); + + await Client.SubscribeToAllAsync( + GroupName, + (s, e, r, ct) => { + counter++; + + switch (counter) { + case 1: s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e); + break; + case > 10: + tcs.TrySetResult(); + break; + } + return Task.CompletedTask; + }, + userCredentials: TestCredentials.Root + ); + + await tcs.Task; + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs index 1414e1599..dbcc44b43 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs @@ -2,76 +2,56 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; -public class happy_case_catching_up_to_link_to_events_manual_ack +public class happy_case_catching_up_to_link_to_events_manual_ack : IClassFixture { - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; + private const string Group = nameof(Group); + private const int BufferCount = 10; + private const int EventWriteCount = BufferCount * 2; - readonly Fixture _fixture; + private readonly Fixture _fixture; public happy_case_catching_up_to_link_to_events_manual_ack(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + public async Task Test() { + await _fixture.Subscription!.Messages.OfType() + .Take(_fixture.Events.Length) + .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + + } public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; + public readonly EventData[] Events; - PersistentSubscription? _subscription; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _events = CreateTestEvents(EventWriteCount) - .Select( - (e, i) => new EventData( - e.EventId, - SystemEventTypes.LinkTo, - Encoding.UTF8.GetBytes($"{i}@test"), - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream - ) - ) + Events = CreateTestEvents(EventWriteCount) + .Select((e, i) => new EventData(e.EventId, SystemEventTypes.LinkTo, Encoding.UTF8.GetBytes($"{i}@test"), + contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream)) .ToArray(); - - _eventsReceived = new(); } - public Task EventsReceived => _eventsReceived.Task; - protected override async Task Given() { - foreach (var e in _events) + foreach (var e in Events) { await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + } - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); + await Client.CreateToAllAsync(Group, new(startFrom: Position.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root); - if (e.OriginalStreamId.StartsWith("test-") - && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); + Subscription = Client.SubscribeToAll(Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); } protected override Task When() => Task.CompletedTask; - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs new file mode 100644 index 000000000..b56ea25a9 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs @@ -0,0 +1,78 @@ +using System.Text; + +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class happy_case_catching_up_to_link_to_events_manual_ack_obsolete + : IClassFixture { + const string Group = nameof(Group); + const int BufferCount = 10; + const int EventWriteCount = BufferCount * 2; + + readonly Fixture _fixture; + + public happy_case_catching_up_to_link_to_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + + public class Fixture : EventStoreClientFixture { + readonly EventData[] _events; + readonly TaskCompletionSource _eventsReceived; + int _eventReceivedCount; + + PersistentSubscription? _subscription; + + public Fixture() { + _events = CreateTestEvents(EventWriteCount) + .Select( + (e, i) => new EventData( + e.EventId, + SystemEventTypes.LinkTo, + Encoding.UTF8.GetBytes($"{i}@test"), + contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream + ) + ) + .ToArray(); + + _eventsReceived = new(); + } + + public Task EventsReceived => _eventsReceived.Task; + + protected override async Task Given() { + foreach (var e in _events) + await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + + await Client.CreateToAllAsync( + Group, + new(startFrom: Position.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToAllAsync( + Group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + + if (e.OriginalStreamId.StartsWith("test-") + && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) + _eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + _eventsReceived.TrySetException(e); + }, + bufferSize: BufferCount, + userCredentials: TestCredentials.Root + ); + } + + protected override Task When() => Task.CompletedTask; + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs index 0eec37c6b..05612443d 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs @@ -1,64 +1,51 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; -public class happy_case_catching_up_to_normal_events_manual_ack : IClassFixture { - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; +public class happy_case_catching_up_to_normal_events_manual_ack + : IClassFixture { + private const string Group = nameof(Group); + private const int BufferCount = 10; + private const int EventWriteCount = BufferCount * 2; - readonly Fixture _fixture; + private readonly Fixture _fixture; public happy_case_catching_up_to_normal_events_manual_ack(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + public async Task Test() { + await _fixture.Subscription!.Messages.OfType() + .Take(_fixture.Events.Length) + .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + } + public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; + public readonly EventData[] Events; - PersistentSubscription? _subscription; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _events = CreateTestEvents(EventWriteCount).ToArray(); - _eventsReceived = new(); + Events = CreateTestEvents(EventWriteCount).ToArray(); } - public Task EventsReceived => _eventsReceived.Task; - protected override async Task Given() { - foreach (var e in _events) + foreach (var e in Events) await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); + await Client.CreateToAllAsync(Group, new(startFrom: Position.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - - if (e.OriginalStreamId.StartsWith("test-") - && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); + Subscription = Client.SubscribeToAll(Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); } protected override Task When() => Task.CompletedTask; - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs new file mode 100644 index 000000000..6431b9514 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs @@ -0,0 +1,65 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class happy_case_catching_up_to_normal_events_manual_ack_obsolete : IClassFixture { + const string Group = nameof(Group); + const int BufferCount = 10; + const int EventWriteCount = BufferCount * 2; + + readonly Fixture _fixture; + + public happy_case_catching_up_to_normal_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + + public class Fixture : EventStoreClientFixture { + readonly EventData[] _events; + readonly TaskCompletionSource _eventsReceived; + int _eventReceivedCount; + + PersistentSubscription? _subscription; + + public Fixture() { + _events = CreateTestEvents(EventWriteCount).ToArray(); + _eventsReceived = new(); + } + + public Task EventsReceived => _eventsReceived.Task; + + protected override async Task Given() { + foreach (var e in _events) + await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + + await Client.CreateToAllAsync( + Group, + new(startFrom: Position.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToAllAsync( + Group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + + if (e.OriginalStreamId.StartsWith("test-") + && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) + _eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + _eventsReceived.TrySetException(e); + }, + bufferSize: BufferCount, + userCredentials: TestCredentials.Root + ); + } + + protected override Task When() => Task.CompletedTask; + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs index c7a5fb7c8..9d89fddb1 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs @@ -1,7 +1,7 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; public class happy_case_filtered : IClassFixture { - readonly Fixture _fixture; + private readonly Fixture _fixture; public happy_case_filtered(Fixture fixture) => _fixture = fixture; @@ -14,58 +14,41 @@ public async Task reads_all_existing_filtered_events(string filterName) { var (getFilter, prepareEvent) = Filters.GetFilter(filterName); var filter = getFilter(streamPrefix); - var appeared = new TaskCompletionSource(); var appearedEvents = new List(); - var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); + var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); - foreach (var e in events) - await _fixture.StreamsClient.AppendToStreamAsync( - $"{streamPrefix}_{Guid.NewGuid():n}", - StreamState.NoStream, - new[] { e } - ); - - await _fixture.Client.CreateToAllAsync( - filterName, - filter, - new(startFrom: Position.Start), - userCredentials: TestCredentials.Root - ); - - using var subscription = await _fixture.Client.SubscribeToAllAsync( - filterName, - async (s, e, r, ct) => { - appearedEvents.Add(e.Event); - if (appearedEvents.Count >= events.Length) - appeared.TrySetResult(true); + foreach (var e in events) { + await _fixture.StreamsClient.AppendToStreamAsync($"{streamPrefix}_{Guid.NewGuid():n}", StreamState.NoStream, + new[] { e }); + } - await s.Ack(e); - }, - userCredentials: TestCredentials.Root - ) + await _fixture.Client.CreateToAllAsync(filterName, filter, new(startFrom: Position.Start), + userCredentials: TestCredentials.Root); + + await using var subscription = + _fixture.Client.SubscribeToAll(filterName, userCredentials: TestCredentials.Root); + await subscription.Messages + .OfType() + .Take(events.Length) + .ForEachAwaitAsync(async e => { + var (resolvedEvent, _) = e; + appearedEvents.Add(resolvedEvent.Event); + await subscription.Ack(resolvedEvent); + }) .WithTimeout(); - await Task.WhenAll(appeared.Task).WithTimeout(); - Assert.Equal(events.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); } public class Fixture : EventStoreClientFixture { protected override async Task Given() { - await StreamsClient.AppendToStreamAsync( - Guid.NewGuid().ToString(), - StreamState.NoStream, - CreateTestEvents(256) - ); + await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, + CreateTestEvents(256)); - await StreamsClient.SetStreamMetadataAsync( - SystemStreams.AllStream, - StreamState.Any, - new(acl: new(SystemRoles.All)), - userCredentials: TestCredentials.Root - ); + await StreamsClient.SetStreamMetadataAsync(SystemStreams.AllStream, StreamState.Any, + new(acl: new(SystemRoles.All)), userCredentials: TestCredentials.Root); } protected override Task When() => Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_obsolete.cs new file mode 100644 index 000000000..aba17050d --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_obsolete.cs @@ -0,0 +1,72 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class happy_case_filtered_obsolete : IClassFixture { + readonly Fixture _fixture; + + public happy_case_filtered_obsolete(Fixture fixture) => _fixture = fixture; + + public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); + + [SupportsPSToAll.Theory] + [MemberData(nameof(FilterCases))] + public async Task reads_all_existing_filtered_events(string filterName) { + var streamPrefix = $"{filterName}-{_fixture.GetStreamName()}"; + var (getFilter, prepareEvent) = Filters.GetFilter(filterName); + var filter = getFilter(streamPrefix); + + var appeared = new TaskCompletionSource(); + var appearedEvents = new List(); + var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); + + foreach (var e in events) + await _fixture.StreamsClient.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + new[] { e } + ); + + await _fixture.Client.CreateToAllAsync( + filterName, + filter, + new(startFrom: Position.Start), + userCredentials: TestCredentials.Root + ); + + using var subscription = await _fixture.Client.SubscribeToAllAsync( + filterName, + async (s, e, r, ct) => { + appearedEvents.Add(e.Event); + if (appearedEvents.Count >= events.Length) + appeared.TrySetResult(true); + + await s.Ack(e); + }, + userCredentials: TestCredentials.Root + ) + .WithTimeout(); + + await Task.WhenAll(appeared.Task).WithTimeout(); + + Assert.Equal(events.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); + } + + public class Fixture : EventStoreClientFixture { + protected override async Task Given() { + await StreamsClient.AppendToStreamAsync( + Guid.NewGuid().ToString(), + StreamState.NoStream, + CreateTestEvents(256) + ); + + await StreamsClient.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.Any, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + } + + protected override Task When() => Task.CompletedTask; + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set.cs index bb4fa655b..065782783 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set.cs @@ -1,7 +1,7 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; public class happy_case_filtered_with_start_from_set : IClassFixture { - readonly Fixture _fixture; + private readonly Fixture _fixture; public happy_case_filtered_with_start_from_set(Fixture fixture) => _fixture = fixture; @@ -14,72 +14,49 @@ public async Task reads_all_existing_filtered_events_from_specified_start(string var (getFilter, prepareEvent) = Filters.GetFilter(filterName); var filter = getFilter(streamPrefix); - var appeared = new TaskCompletionSource(); - var appearedEvents = new List(); - var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); - var eventsToSkip = events.Take(10).ToArray(); + var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); + var eventsToSkip = events.Take(10).ToArray(); var eventsToCapture = events.Skip(10).ToArray(); IWriteResult? eventToCaptureResult = null; - foreach (var e in eventsToSkip) - await _fixture.StreamsClient.AppendToStreamAsync( - $"{streamPrefix}_{Guid.NewGuid():n}", - StreamState.NoStream, - new[] { e } - ); + foreach (var e in eventsToSkip) { + await _fixture.StreamsClient.AppendToStreamAsync($"{streamPrefix}_{Guid.NewGuid():n}", StreamState.NoStream, + new[] { e }); + } foreach (var e in eventsToCapture) { - var result = await _fixture.StreamsClient.AppendToStreamAsync( - $"{streamPrefix}_{Guid.NewGuid():n}", - StreamState.NoStream, - new[] { e } - ); + var result = await _fixture.StreamsClient.AppendToStreamAsync($"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, new[] { e }); eventToCaptureResult ??= result; } - await _fixture.Client.CreateToAllAsync( - filterName, - filter, - new(startFrom: eventToCaptureResult!.LogPosition), - userCredentials: TestCredentials.Root - ); - - using var subscription = await _fixture.Client.SubscribeToAllAsync( - filterName, - async (s, e, r, ct) => { - appearedEvents.Add(e.Event); - if (appearedEvents.Count >= eventsToCapture.Length) - appeared.TrySetResult(true); - - await s.Ack(e); - }, - userCredentials: TestCredentials.Root - ) - .WithTimeout(); + await _fixture.Client.CreateToAllAsync(filterName, filter, new(startFrom: eventToCaptureResult!.LogPosition), + userCredentials: TestCredentials.Root); - await Task.WhenAll(appeared.Task).WithTimeout(); + await using var subscription = + _fixture.Client.SubscribeToAll(filterName, userCredentials: TestCredentials.Root); + + var appearedEvents = await subscription.Messages.OfType() + .Take(10) + .Select(e => e.ResolvedEvent.Event) + .ToArrayAsync() + .AsTask() + .WithTimeout(); Assert.Equal(eventsToCapture.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); } public class Fixture : EventStoreClientFixture { protected override async Task Given() { - await StreamsClient.AppendToStreamAsync( - Guid.NewGuid().ToString(), - StreamState.NoStream, - CreateTestEvents(256) - ); - - await StreamsClient.SetStreamMetadataAsync( - SystemStreams.AllStream, - StreamState.Any, - new(acl: new(SystemRoles.All)), - userCredentials: TestCredentials.Root - ); + await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, + CreateTestEvents(256)); + + await StreamsClient.SetStreamMetadataAsync(SystemStreams.AllStream, StreamState.Any, + new(acl: new(SystemRoles.All)), userCredentials: TestCredentials.Root); } protected override Task When() => Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set_obsolete.cs new file mode 100644 index 000000000..bf8b140a4 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set_obsolete.cs @@ -0,0 +1,86 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class happy_case_filtered_with_start_from_set_obsolete : IClassFixture { + readonly Fixture _fixture; + + public happy_case_filtered_with_start_from_set_obsolete(Fixture fixture) => _fixture = fixture; + + public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); + + [SupportsPSToAll.Theory] + [MemberData(nameof(FilterCases))] + public async Task reads_all_existing_filtered_events_from_specified_start(string filterName) { + var streamPrefix = $"{filterName}-{_fixture.GetStreamName()}"; + var (getFilter, prepareEvent) = Filters.GetFilter(filterName); + var filter = getFilter(streamPrefix); + + var appeared = new TaskCompletionSource(); + var appearedEvents = new List(); + var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); + var eventsToSkip = events.Take(10).ToArray(); + var eventsToCapture = events.Skip(10).ToArray(); + + IWriteResult? eventToCaptureResult = null; + + foreach (var e in eventsToSkip) + await _fixture.StreamsClient.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + new[] { e } + ); + + foreach (var e in eventsToCapture) { + var result = await _fixture.StreamsClient.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + new[] { e } + ); + + eventToCaptureResult ??= result; + } + + await _fixture.Client.CreateToAllAsync( + filterName, + filter, + new(startFrom: eventToCaptureResult!.LogPosition), + userCredentials: TestCredentials.Root + ); + + using var subscription = await _fixture.Client.SubscribeToAllAsync( + filterName, + async (s, e, r, ct) => { + appearedEvents.Add(e.Event); + if (appearedEvents.Count >= eventsToCapture.Length) + appeared.TrySetResult(true); + + await s.Ack(e); + }, + userCredentials: TestCredentials.Root + ) + .WithTimeout(); + + await Task.WhenAll(appeared.Task).WithTimeout(); + + Assert.Equal(eventsToCapture.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); + } + + public class Fixture : EventStoreClientFixture { + protected override async Task Given() { + await StreamsClient.AppendToStreamAsync( + Guid.NewGuid().ToString(), + StreamState.NoStream, + CreateTestEvents(256) + ); + + await StreamsClient.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.Any, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + } + + protected override Task When() => Task.CompletedTask; + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs index 5b7db42d0..100e49f27 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs @@ -2,63 +2,56 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; public class happy_case_writing_and_subscribing_to_normal_events_manual_ack : IClassFixture { - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; + private const string Group = nameof(Group); + private const int BufferCount = 10; + private const int EventWriteCount = BufferCount * 2; - readonly Fixture _fixture; + private readonly Fixture _fixture; public happy_case_writing_and_subscribing_to_normal_events_manual_ack(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + public async Task Test() { + await _fixture.Subscription!.Messages.OfType() + .SelectAwait(async e => { + await _fixture.Subscription.Ack(e.ResolvedEvent); + return e; + }) + .Where(e => e.ResolvedEvent.OriginalStreamId.StartsWith("test-")) + .Take(_fixture.Events.Length) + .ToArrayAsync() + .AsTask() + .WithTimeout(); + } public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; + public readonly EventData[] Events; - PersistentSubscription? _subscription; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _events = CreateTestEvents(EventWriteCount).ToArray(); - _eventsReceived = new(); + Events = CreateTestEvents(EventWriteCount).ToArray(); } - public Task EventsReceived => _eventsReceived.Task; - protected override async Task Given() { - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.End, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); + await Client.CreateToAllAsync(Group, new(startFrom: Position.End, resolveLinkTos: true), + userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - if (e.OriginalStreamId.StartsWith("test-") - && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); + Subscription = Client.SubscribeToAll(Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); } protected override async Task When() { - foreach (var e in _events) + foreach (var e in Events) { await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + } } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs new file mode 100644 index 000000000..42b5a7325 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs @@ -0,0 +1,65 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete + : IClassFixture { + const string Group = nameof(Group); + const int BufferCount = 10; + const int EventWriteCount = BufferCount * 2; + + readonly Fixture _fixture; + + public happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + + public class Fixture : EventStoreClientFixture { + readonly EventData[] _events; + readonly TaskCompletionSource _eventsReceived; + int _eventReceivedCount; + + PersistentSubscription? _subscription; + + public Fixture() { + _events = CreateTestEvents(EventWriteCount).ToArray(); + _eventsReceived = new(); + } + + public Task EventsReceived => _eventsReceived.Task; + + protected override async Task Given() { + await Client.CreateToAllAsync( + Group, + new(startFrom: Position.End, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToAllAsync( + Group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + if (e.OriginalStreamId.StartsWith("test-") + && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) + _eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + _eventsReceived.TrySetException(e); + }, + bufferSize: BufferCount, + userCredentials: TestCredentials.Root + ); + } + + protected override async Task When() { + foreach (var e in _events) + await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + } + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_filtered_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_filtered_obsolete.cs new file mode 100644 index 000000000..67b13a197 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_filtered_obsolete.cs @@ -0,0 +1,30 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +public class update_existing_filtered_obsolete + : IClassFixture { + const string Group = "existing-filtered"; + + readonly Fixture _fixture; + + public update_existing_filtered_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task the_completion_succeeds() => + await _fixture.Client.UpdateToAllAsync( + Group, + new(true), + userCredentials: TestCredentials.Root + ); + + public class Fixture : EventStoreClientFixture { + protected override async Task Given() => + await Client.CreateToAllAsync( + Group, + EventTypeFilter.Prefix("prefix-filter-"), + new(), + userCredentials: TestCredentials.Root + ); + + protected override Task When() => Task.CompletedTask; + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point.cs index 741acd876..05d6a70e3 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point.cs @@ -1,124 +1,108 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; -public class update_existing_with_check_point - : IClassFixture { - const string Group = "existing-with-check-point"; +public class update_existing_with_check_point : IClassFixture { + private const string Group = "existing-with-check-point"; - readonly Fixture _fixture; + private readonly Fixture _fixture; public update_existing_with_check_point(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] - public async Task resumes_from_check_point() { - var resumedEvent = await _fixture.Resumed.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.True(resumedEvent.Event.Position > _fixture.CheckPoint); + public void resumes_from_check_point() { + Assert.True(_fixture.Resumed.Event.Position > _fixture.CheckPoint); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _appeared; - readonly List _appearedEvents; - readonly TaskCompletionSource _checkPointSource; + private readonly List _appearedEvents; + private readonly EventData[] _events; - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; - readonly EventData[] _events; - readonly TaskCompletionSource _resumedSource; + private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; + private IAsyncEnumerator? _enumerator; - StreamSubscription? _checkPointSubscription; - PersistentSubscription? _firstSubscription; - PersistentSubscription? _secondSubscription; + public ResolvedEvent Resumed { get; private set; } + public Position CheckPoint { get; private set; } public Fixture() { - _droppedSource = new(); - _resumedSource = new(); - _checkPointSource = new(); - _appeared = new(); - _appearedEvents = new(); - _events = CreateTestEvents(5).ToArray(); + _appearedEvents = new(); + _events = CreateTestEvents(5).ToArray(); } - public Task Resumed => _resumedSource.Task; - public Position CheckPoint { get; private set; } - protected override async Task Given() { - foreach (var e in _events) + foreach (var e in _events) { await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - - await Client.CreateToAllAsync( - Group, - new( - checkPointLowerBound: 5, - checkPointAfter: TimeSpan.FromSeconds(1), - startFrom: Position.Start - ), - userCredentials: TestCredentials.Root - ); - - var checkPointStream = $"$persistentsubscription-$all::{Group}-checkpoint"; - _checkPointSubscription = await StreamsClient.SubscribeToStreamAsync( - checkPointStream, - FromStream.Start, - (_, e, _) => { - _checkPointSource.TrySetResult(e); - return Task.CompletedTask; - }, - subscriptionDropped: (_, reason, ex) => { - if (ex is not null) - _checkPointSource.TrySetException(ex); - else - _checkPointSource.TrySetResult(default); - }, - userCredentials: TestCredentials.Root - ); - - _firstSubscription = await Client.SubscribeToAllAsync( - Group, - async (s, e, r, ct) => { - _appearedEvents.Add(e); - - if (_appearedEvents.Count == _events.Length) - _appeared.TrySetResult(true); - - await s.Ack(e); - }, - (subscription, reason, ex) => _droppedSource.TrySetResult((reason, ex)), - TestCredentials.Root - ); - - await Task.WhenAll(_appeared.Task, _checkPointSource.Task).WithTimeout(); - - CheckPoint = _checkPointSource.Task.Result.Event.Data.ParsePosition(); - } + } + + await Client.CreateToAllAsync(Group, + new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: Position.Start), + userCredentials: TestCredentials.Root); + + _subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); + _enumerator = _subscription.Messages.GetAsyncEnumerator(); + await _enumerator.MoveNextAsync(); + + await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); + + return; + + async Task Subscribe() { + while (await _enumerator.MoveNextAsync()) { + if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + _appearedEvents.Add(resolvedEvent); + await _subscription.Ack(resolvedEvent); + if (_appearedEvents.Count == _events.Length) { + break; + } + } + } + + async Task WaitForCheckpoint() { + await using var subscription = StreamsClient.SubscribeToStream( + $"$persistentsubscription-$all::{Group}-checkpoint", FromStream.Start, + userCredentials: TestCredentials.Root); + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + CheckPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } } protected override async Task When() { // Force restart of the subscription await Client.UpdateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - await _droppedSource.Task.WithTimeout(); - - _secondSubscription = await Client.SubscribeToAllAsync( - Group, - async (s, e, r, ct) => { - _resumedSource.TrySetResult(e); - await s.Ack(e); - }, - (_, reason, ex) => { - if (ex is not null) - _resumedSource.TrySetException(ex); - else - _resumedSource.TrySetResult(default); - }, - TestCredentials.Root - ); - - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + try { + while (await _enumerator!.MoveNextAsync()) { } + } catch (PersistentSubscriptionDroppedByServerException) { } + + foreach (var e in _events) { + await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); + } + + await using var subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); + Resumed = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .Take(1) + .FirstAsync() + .AsTask() + .WithTimeout(); } - public override Task DisposeAsync() { - _firstSubscription?.Dispose(); - _secondSubscription?.Dispose(); - _checkPointSubscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (_enumerator is not null) { + await _enumerator.DisposeAsync(); + } + + if (_subscription is not null) { + await _subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered.cs index 33738ba9b..829aca768 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered.cs @@ -2,125 +2,110 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; public class update_existing_with_check_point_filtered : IClassFixture { - const string Group = "existing-with-check-point-filtered"; + private const string Group = "existing-with-check-point-filtered"; - readonly Fixture _fixture; + private readonly Fixture _fixture; public update_existing_with_check_point_filtered(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] - public async Task resumes_from_check_point() { - var resumedEvent = await _fixture.Resumed.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.True(resumedEvent.Event.Position > _fixture.CheckPoint); + public void resumes_from_check_point() { + Assert.True(_fixture.Resumed.Event.Position > _fixture.CheckPoint); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _appeared; - readonly List _appearedEvents; - readonly TaskCompletionSource _checkPointSource; - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; - readonly EventData[] _events; - readonly TaskCompletionSource _resumedSource; + private readonly List _appearedEvents; + private readonly EventData[] _events; - StreamSubscription? _checkPointSubscription; - PersistentSubscription? _firstSubscription; - PersistentSubscription? _secondSubscription; + private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; + private IAsyncEnumerator? _enumerator; + + public ResolvedEvent Resumed { get; private set; } + public Position CheckPoint { get; private set; } public Fixture() { - _droppedSource = new(); - _resumedSource = new(); - _checkPointSource = new(); - _appeared = new(); - _appearedEvents = new(); - _events = CreateTestEvents(5).ToArray(); + _appearedEvents = new(); + _events = CreateTestEvents(5).ToArray(); } - public Task Resumed => _resumedSource.Task; - - public Position CheckPoint { get; private set; } protected override async Task Given() { - foreach (var e in _events) + foreach (var e in _events) { await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); - - await Client.CreateToAllAsync( - Group, - StreamFilter.Prefix("test"), - new( - checkPointLowerBound: 5, - checkPointAfter: TimeSpan.FromSeconds(1), - startFrom: Position.Start - ), - userCredentials: TestCredentials.Root - ); - - var checkPointStream = $"$persistentsubscription-$all::{Group}-checkpoint"; - _checkPointSubscription = await StreamsClient.SubscribeToStreamAsync( - checkPointStream, - FromStream.Start, - (_, e, ct) => { - _checkPointSource.TrySetResult(e); - return Task.CompletedTask; - }, - subscriptionDropped: (_, reason, ex) => { - if (ex is not null) - _checkPointSource.TrySetException(ex); - else - _checkPointSource.TrySetResult(default); - }, - userCredentials: TestCredentials.Root - ); - - _firstSubscription = await Client.SubscribeToAllAsync( - Group, - async (s, e, r, ct) => { - _appearedEvents.Add(e); - - if (_appearedEvents.Count == _events.Length) - _appeared.TrySetResult(true); - - await s.Ack(e); - }, - (subscription, reason, ex) => _droppedSource.TrySetResult((reason, ex)), - TestCredentials.Root - ); - - await Task.WhenAll(_appeared.Task, _checkPointSource.Task).WithTimeout(); - - CheckPoint = _checkPointSource.Task.Result.Event.Data.ParsePosition(); + } + + await Client.CreateToAllAsync(Group, StreamFilter.Prefix("test"), + new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: Position.Start), + userCredentials: TestCredentials.Root); + + _subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); + _enumerator = _subscription.Messages.GetAsyncEnumerator(); + await _enumerator.MoveNextAsync(); + + await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); + + return; + + async Task Subscribe() { + while (await _enumerator.MoveNextAsync()) { + if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + _appearedEvents.Add(resolvedEvent); + await _subscription.Ack(resolvedEvent); + if (_appearedEvents.Count == _events.Length) { + break; + } + } + } + + async Task WaitForCheckpoint() { + await using var subscription = StreamsClient.SubscribeToStream( + $"$persistentsubscription-$all::{Group}-checkpoint", FromStream.Start, + userCredentials: TestCredentials.Root); + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + CheckPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } } protected override async Task When() { // Force restart of the subscription await Client.UpdateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - await _droppedSource.Task.WithTimeout(); - - _secondSubscription = await Client.SubscribeToAllAsync( - Group, - async (s, e, r, ct) => { - _resumedSource.TrySetResult(e); - await s.Ack(e); - s.Dispose(); - }, - (_, reason, ex) => { - if (ex is not null) - _resumedSource.TrySetException(ex); - else - _resumedSource.TrySetResult(default); - }, - TestCredentials.Root - ); - - foreach (var e in _events) + try { + while (await _enumerator!.MoveNextAsync()) { } + } catch (PersistentSubscriptionDroppedByServerException) { } + + foreach (var e in _events) { await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); + } + + await using var subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); + Resumed = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .Take(1) + .FirstAsync() + .AsTask() + .WithTimeout(); } - public override Task DisposeAsync() { - _firstSubscription?.Dispose(); - _secondSubscription?.Dispose(); - _checkPointSubscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (_enumerator is not null) { + await _enumerator.DisposeAsync(); + } + + if (_subscription is not null) { + await _subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered_obsolete.cs new file mode 100644 index 000000000..ddb7445f6 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered_obsolete.cs @@ -0,0 +1,120 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class update_existing_with_check_point_filtered_obsolete + : IClassFixture { + const string Group = "existing-with-check-point-filtered"; + + readonly Fixture _fixture; + + public update_existing_with_check_point_filtered_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task resumes_from_check_point() { + var resumedEvent = await _fixture.Resumed.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.True(resumedEvent.Event.Position > _fixture.CheckPoint); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _appeared; + readonly List _appearedEvents; + readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; + readonly EventData[] _events; + readonly TaskCompletionSource _resumedSource; + + PersistentSubscription? _firstSubscription; + PersistentSubscription? _secondSubscription; + + public Fixture() { + _droppedSource = new(); + _resumedSource = new(); + _appeared = new(); + _appearedEvents = new(); + _events = CreateTestEvents(5).ToArray(); + } + + public Task Resumed => _resumedSource.Task; + + public Position CheckPoint { get; private set; } + + protected override async Task Given() { + foreach (var e in _events) + await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); + + await Client.CreateToAllAsync( + Group, + StreamFilter.Prefix("test"), + new( + checkPointLowerBound: 5, + checkPointAfter: TimeSpan.FromSeconds(1), + startFrom: Position.Start + ), + userCredentials: TestCredentials.Root + ); + + _firstSubscription = await Client.SubscribeToAllAsync( + Group, + async (s, e, r, ct) => { + _appearedEvents.Add(e); + + if (_appearedEvents.Count == _events.Length) + _appeared.TrySetResult(true); + + await s.Ack(e); + }, + (subscription, reason, ex) => _droppedSource.TrySetResult((reason, ex)), + TestCredentials.Root + ); + + await Task.WhenAll(_appeared.Task, Checkpointed()).WithTimeout(); + + return; + + async Task Checkpointed() { + await using var subscription = StreamsClient.SubscribeToStream( + $"$persistentsubscription-$all::{Group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root); + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + CheckPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } + + protected override async Task When() { + // Force restart of the subscription + await Client.UpdateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); + + await _droppedSource.Task.WithTimeout(); + + _secondSubscription = await Client.SubscribeToAllAsync( + Group, + async (s, e, r, ct) => { + _resumedSource.TrySetResult(e); + await s.Ack(e); + s.Dispose(); + }, + (_, reason, ex) => { + if (ex is not null) + _resumedSource.TrySetException(ex); + else + _resumedSource.TrySetResult(default); + }, + TestCredentials.Root + ); + + foreach (var e in _events) + await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); + } + + public override Task DisposeAsync() { + _firstSubscription?.Dispose(); + _secondSubscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs index 82c0ec320..71e89d260 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs @@ -1,61 +1,49 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; -public class update_existing_with_subscribers - : IClassFixture { - const string Group = "existing"; +public class update_existing_with_subscribers : IClassFixture { + private const string Group = "existing"; - readonly Fixture _fixture; + private readonly Fixture _fixture; public update_existing_with_subscribers(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] public async Task existing_subscriptions_are_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); + var ex = await Assert.ThrowsAsync(async () => { + while (await _fixture.Enumerator!.MoveNextAsync()) { + } + }).WithTimeout(); -#if NET Assert.Equal(SystemStreams.AllStream, ex.StreamName); Assert.Equal(Group, ex.GroupName); -#endif } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; - PersistentSubscription? _subscription; + private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; + public IAsyncEnumerator? Enumerator { get; private set; } - public Fixture() => _droppedSource = new(); + protected override async Task Given() { + await Client.CreateToAllAsync(Group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _droppedSource.Task; + _subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - protected override async Task Given() { - await Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - (subscription, reason, ex) => _droppedSource.TrySetResult((reason, ex)), - TestCredentials.Root - ); - - // todo: investigate why this test is flaky without this delay - await Task.Delay(500); + Enumerator = _subscription.Messages.GetAsyncEnumerator(); + + await Enumerator.MoveNextAsync(); } - protected override Task When() => - Client.UpdateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); + protected override Task When() => Client.UpdateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); + + public override async Task DisposeAsync() { + if (Enumerator is not null) { + await Enumerator.DisposeAsync(); + } + + if (_subscription is not null) { + await _subscription.DisposeAsync(); + } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers_obsolete.cs new file mode 100644 index 000000000..0be68cf78 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers_obsolete.cs @@ -0,0 +1,60 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class update_existing_with_subscribers_obsolete + : IClassFixture { + const string Group = "existing"; + + readonly Fixture _fixture; + + public update_existing_with_subscribers_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task existing_subscriptions_are_dropped() { + var (reason, exception) = await _fixture.Dropped.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(Group, ex.GroupName); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; + PersistentSubscription? _subscription; + + public Fixture() => _droppedSource = new(); + + public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _droppedSource.Task; + + protected override async Task Given() { + await Client.CreateToAllAsync( + Group, + new(), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToAllAsync( + Group, + delegate { return Task.CompletedTask; }, + (subscription, reason, ex) => _droppedSource.TrySetResult((reason, ex)), + TestCredentials.Root + ); + + // todo: investigate why this test is flaky without this delay + await Task.Delay(500); + } + + protected override Task When() => + Client.UpdateToAllAsync( + Group, + new(), + userCredentials: TestCredentials.Root + ); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events.cs index 8d35bf318..cda94d8b3 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events.cs @@ -1,109 +1,87 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; -public class when_writing_and_filtering_out_events - : IClassFixture { - const string Group = "filtering-out-events"; +public class when_writing_and_filtering_out_events : IClassFixture { + private const string Group = "filtering-out-events"; - readonly Fixture _fixture; + private readonly Fixture _fixture; public when_writing_and_filtering_out_events(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] - public async Task it_should_write_a_check_point() { - await _fixture.SecondCheckPoint.WithTimeout(); - var secondCheckPoint = _fixture.SecondCheckPoint.Result.Event.Data.ParsePosition(); - Assert.True(secondCheckPoint > _fixture.FirstCheckPoint); - Assert.Equal( - _fixture.Events.Select(e => e.EventId), - _fixture.AppearedEvents.Select(e => e.Event.EventId) - ); + public void it_should_write_a_check_point() { + Assert.True(_fixture.SecondCheckPoint > _fixture.FirstCheckPoint); + Assert.Equal(_fixture.Events.Select(e => e.EventId), _fixture.AppearedEvents.Select(e => e.Event.EventId)); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _appeared; - readonly List _appearedEvents, _checkPoints; - readonly string _checkPointStream = $"$persistentsubscription-$all::{Group}-checkpoint"; - readonly EventData[] _events; - - readonly TaskCompletionSource _firstCheckPointSource, _secondCheckPointSource; - StreamSubscription? _checkPointSubscription; - PersistentSubscription? _subscription; + private readonly List _appearedEvents; public Fixture() { - _firstCheckPointSource = new(); - _secondCheckPointSource = new(); - _appeared = new(); - _appearedEvents = new(); - _checkPoints = new(); - _events = CreateTestEvents(5).ToArray(); + Events = CreateTestEvents(10).ToArray(); + _appearedEvents = new(); } - public Task SecondCheckPoint => _secondCheckPointSource.Task; - public Position FirstCheckPoint { get; private set; } - public EventData[] Events => _events.ToArray(); - public ResolvedEvent[] AppearedEvents => _appearedEvents.ToArray(); + public Position SecondCheckPoint { get; private set; } + public Position FirstCheckPoint { get; private set; } + public EventData[] Events { get; } + public ResolvedEvent[] AppearedEvents => _appearedEvents.ToArray(); protected override async Task Given() { - foreach (var e in _events) + foreach (var e in Events) { await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - - await Client.CreateToAllAsync( - Group, - StreamFilter.Prefix("test"), - new( - checkPointLowerBound: 5, - checkPointAfter: TimeSpan.FromSeconds(1), - startFrom: Position.Start - ), - userCredentials: TestCredentials.Root - ); - - _checkPointSubscription = await StreamsClient.SubscribeToStreamAsync( - _checkPointStream, - FromStream.Start, - (_, e, _) => { - if (_checkPoints.Count == 0) - _firstCheckPointSource.TrySetResult(e); - else - _secondCheckPointSource.TrySetResult(e); - - _checkPoints.Add(e); - return Task.CompletedTask; - }, - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (s, e, r, ct) => { - _appearedEvents.Add(e); - - if (_appearedEvents.Count == _events.Length) - _appeared.TrySetResult(true); - - await s.Ack(e); - }, - userCredentials: TestCredentials.Root - ); - - await Task.WhenAll(_appeared.Task, _firstCheckPointSource.Task).WithTimeout(); - - FirstCheckPoint = _firstCheckPointSource.Task.Result.Event.Data.ParsePosition(); + } + + await Client.CreateToAllAsync(Group, StreamFilter.Prefix("test"), + new(checkPointLowerBound: 1, checkPointUpperBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), + startFrom: Position.Start), userCredentials: TestCredentials.Root); + + await using var subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoints().WithTimeout()); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + _appearedEvents.Add(resolvedEvent); + await subscription.Ack(resolvedEvent); + if (_appearedEvents.Count == Events.Length) { + break; + } + } + } + + async Task WaitForCheckpoints() { + bool firstCheckpointSet = false; + await using var subscription = StreamsClient.SubscribeToStream( + $"$persistentsubscription-$all::{Group}-checkpoint", FromStream.Start, + userCredentials: TestCredentials.Root); + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + if (!firstCheckpointSet) { + FirstCheckPoint = resolvedEvent.Event.Data.ParsePosition(); + firstCheckpointSet = true; + } else { + SecondCheckPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } } protected override async Task When() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync( - "filtered-out-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { e } - ); - } - - public override Task DisposeAsync() { - _subscription?.Dispose(); - _checkPointSubscription?.Dispose(); - return base.DisposeAsync(); + foreach (var e in Events) { + await StreamsClient.AppendToStreamAsync("filtered-out-stream-" + Guid.NewGuid(), StreamState.Any, + new[] { e }); + } } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events_obsolete.cs new file mode 100644 index 000000000..4e1018ffe --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events_obsolete.cs @@ -0,0 +1,108 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class when_writing_and_filtering_out_events_obsolete + : IClassFixture { + const string Group = "filtering-out-events"; + + readonly Fixture _fixture; + + public when_writing_and_filtering_out_events_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task it_should_write_a_check_point() { + await Task.Yield(); + Assert.True(_fixture.SecondCheckPoint > _fixture.FirstCheckPoint); + Assert.Equal( + _fixture.Events.Select(e => e.EventId), + _fixture.AppearedEvents.Select(e => e.Event.EventId) + ); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _appeared; + readonly List _appearedEvents, _checkPoints; + readonly EventData[] _events; + + PersistentSubscription? _subscription; + + public Fixture() { + _appeared = new(); + _appearedEvents = new(); + _checkPoints = new(); + _events = CreateTestEvents(10).ToArray(); + } + + public Position SecondCheckPoint { get; private set; } + public Position FirstCheckPoint { get; private set; } + public EventData[] Events => _events.ToArray(); + public ResolvedEvent[] AppearedEvents => _appearedEvents.ToArray(); + + protected override async Task Given() { + foreach (var e in _events) + await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + + await Client.CreateToAllAsync( + Group, + StreamFilter.Prefix("test"), + new( + checkPointLowerBound: 1, + checkPointUpperBound: 5, + checkPointAfter: TimeSpan.FromSeconds(1), + startFrom: Position.Start + ), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToAllAsync( + Group, + async (s, e, r, ct) => { + _appearedEvents.Add(e); + + if (_appearedEvents.Count == _events.Length) + _appeared.TrySetResult(true); + + await s.Ack(e); + }, + userCredentials: TestCredentials.Root + ); + + await Task.WhenAll(_appeared.Task, Checkpointed()).WithTimeout(); + + async Task Checkpointed() { + bool firstCheckpointSet = false; + await using var subscription = StreamsClient.SubscribeToStream( + $"$persistentsubscription-$all::{Group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root); + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + if (!firstCheckpointSet) { + FirstCheckPoint = resolvedEvent.Event.Data.ParsePosition(); + firstCheckpointSet = true; + } else { + SecondCheckPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } + } + + protected override async Task When() { + foreach (var e in _events) + await StreamsClient.AppendToStreamAsync( + "filtered-out-stream-" + Guid.NewGuid(), + StreamState.Any, + new[] { e } + ); + } + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack.cs index a5b001902..72e0cfd00 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack.cs @@ -2,66 +2,56 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; public class when_writing_and_subscribing_to_normal_events_manual_nack : IClassFixture { - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; + private const string Group = nameof(Group); + private const int BufferCount = 10; + private const int EventWriteCount = BufferCount * 2; - readonly Fixture _fixture; + private readonly Fixture _fixture; public when_writing_and_subscribing_to_normal_events_manual_nack(Fixture fixture) => _fixture = fixture; [SupportsPSToAll.Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + public async Task Test() { + await _fixture.Subscription!.Messages.OfType() + .SelectAwait(async e => { + await _fixture.Subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e.ResolvedEvent); + return e; + }) + .Where(e => e.ResolvedEvent.OriginalStreamId.StartsWith("test-")) + .Take(_fixture.Events.Length) + .ToArrayAsync() + .AsTask() + .WithTimeout(); + } public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; + public readonly EventData[] Events; - PersistentSubscription? _subscription; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _events = CreateTestEvents(EventWriteCount) - .ToArray(); - - _eventsReceived = new(); + Events = CreateTestEvents(EventWriteCount).ToArray(); } - public Task EventsReceived => _eventsReceived.Task; - protected override async Task Given() { - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); + await Client.CreateToAllAsync(Group, new(startFrom: Position.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e); - - if (e.OriginalStreamId.StartsWith("test-") - && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); + Subscription = Client.SubscribeToAll(Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); } protected override async Task When() { - foreach (var e in _events) + foreach (var e in Events) { await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + } } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs new file mode 100644 index 000000000..c1ff885a3 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs @@ -0,0 +1,68 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; + +[Obsolete] +public class when_writing_and_subscribing_to_normal_events_manual_nack_obsolete + : IClassFixture { + const string Group = nameof(Group); + const int BufferCount = 10; + const int EventWriteCount = BufferCount * 2; + + readonly Fixture _fixture; + + public when_writing_and_subscribing_to_normal_events_manual_nack_obsolete(Fixture fixture) => _fixture = fixture; + + [SupportsPSToAll.Fact] + public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + + public class Fixture : EventStoreClientFixture { + readonly EventData[] _events; + readonly TaskCompletionSource _eventsReceived; + int _eventReceivedCount; + + PersistentSubscription? _subscription; + + public Fixture() { + _events = CreateTestEvents(EventWriteCount) + .ToArray(); + + _eventsReceived = new(); + } + + public Task EventsReceived => _eventsReceived.Task; + + protected override async Task Given() { + await Client.CreateToAllAsync( + Group, + new(startFrom: Position.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToAllAsync( + Group, + async (subscription, e, retryCount, ct) => { + await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e); + + if (e.OriginalStreamId.StartsWith("test-") + && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) + _eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + _eventsReceived.TrySetException(e); + }, + bufferSize: BufferCount, + userCredentials: TestCredentials.Root + ); + } + + protected override async Task When() { + foreach (var e in _events) + await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + } + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client.cs index 6515ab9ad..eb5e442ae 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client.cs @@ -2,45 +2,32 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; public class connect_to_existing_with_max_one_client : IClassFixture { - const string Group = "startinbeginning1"; - const string Stream = nameof(connect_to_existing_with_max_one_client); - readonly Fixture _fixture; + private const string Group = "startinbeginning1"; + private const string Stream = nameof(connect_to_existing_with_max_one_client); + private readonly Fixture _fixture; public connect_to_existing_with_max_one_client(Fixture fixture) => _fixture = fixture; [Fact] public async Task the_second_subscription_fails_to_connect() { - using var first = await _fixture.Client.SubscribeToStreamAsync( - Stream, - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ).WithTimeout(); - var ex = await Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToStreamAsync( - Stream, - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ); - } - ).WithTimeout(); + () => Task.WhenAll(Subscribe().WithTimeout(), Subscribe().WithTimeout())); Assert.Equal(Stream, ex.StreamName); Assert.Equal(Group, ex.GroupName); + return; + + async Task Subscribe() { + await using var subscription = + _fixture.Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); + await subscription.Messages.AnyAsync(); + } } public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateToStreamAsync( - Stream, - Group, - new(maxSubscriberCount: 1), - userCredentials: TestCredentials.Root - ); + protected override Task Given() => Client.CreateToStreamAsync(Stream, Group, new(maxSubscriberCount: 1), + userCredentials: TestCredentials.Root); protected override Task When() => Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client_obsolete.cs new file mode 100644 index 000000000..94dce7f5a --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client_obsolete.cs @@ -0,0 +1,47 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class connect_to_existing_with_max_one_client_obsolete + : IClassFixture { + const string Group = "startinbeginning1"; + const string Stream = nameof(connect_to_existing_with_max_one_client_obsolete); + readonly Fixture _fixture; + + public connect_to_existing_with_max_one_client_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task the_second_subscription_fails_to_connect() { + using var first = await _fixture.Client.SubscribeToStreamAsync( + Stream, + Group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ).WithTimeout(); + + var ex = await Assert.ThrowsAsync( + async () => { + using var _ = await _fixture.Client.SubscribeToStreamAsync( + Stream, + Group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ); + } + ).WithTimeout(); + + Assert.Equal(Stream, ex.StreamName); + Assert.Equal(Group, ex.GroupName); + } + + public class Fixture : EventStoreClientFixture { + protected override Task Given() => + Client.CreateToStreamAsync( + Stream, + Group, + new(maxSubscriberCount: 1), + userCredentials: TestCredentials.Root + ); + + protected override Task When() => Task.CompletedTask; + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions.cs index b607b77fd..70f4b51f0 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions.cs @@ -2,37 +2,25 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; public class connect_to_existing_with_permissions : IClassFixture { - const string Stream = nameof(connect_to_existing_with_permissions); + private const string Stream = nameof(connect_to_existing_with_permissions); - readonly Fixture _fixture; + private readonly Fixture _fixture; public connect_to_existing_with_permissions(Fixture fixture) => _fixture = fixture; [Fact] public async Task the_subscription_succeeds() { - var dropped = new TaskCompletionSource<(SubscriptionDroppedReason, Exception?)>(); - using var subscription = await _fixture.Client.SubscribeToStreamAsync( - Stream, - "agroupname17", - delegate { return Task.CompletedTask; }, - (s, reason, ex) => dropped.TrySetResult((reason, ex)), - TestCredentials.Root - ).WithTimeout(); + await using var subscription = + _fixture.Client.SubscribeToStream(Stream, "agroupname17", userCredentials: TestCredentials.Root); - Assert.NotNull(subscription); - - await Assert.ThrowsAsync(() => dropped.Task.WithTimeout()); + Assert.True(await subscription.Messages + .FirstAsync().AsTask().WithTimeout() is PersistentSubscriptionMessage.SubscriptionConfirmation); } public class Fixture : EventStoreClientFixture { protected override Task Given() => - Client.CreateToStreamAsync( - Stream, - "agroupname17", - new(), - userCredentials: TestCredentials.Root - ); + Client.CreateToStreamAsync(Stream, "agroupname17", new(), userCredentials: TestCredentials.Root); protected override Task When() => Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions_obsolete.cs new file mode 100644 index 000000000..423c48303 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions_obsolete.cs @@ -0,0 +1,39 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class connect_to_existing_with_permissions_obsolete + : IClassFixture { + const string Stream = nameof(connect_to_existing_with_permissions_obsolete); + + readonly Fixture _fixture; + + public connect_to_existing_with_permissions_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task the_subscription_succeeds() { + var dropped = new TaskCompletionSource<(SubscriptionDroppedReason, Exception?)>(); + using var subscription = await _fixture.Client.SubscribeToStreamAsync( + Stream, + "agroupname17", + delegate { return Task.CompletedTask; }, + (s, reason, ex) => dropped.TrySetResult((reason, ex)), + TestCredentials.Root + ).WithTimeout(); + + Assert.NotNull(subscription); + + await Assert.ThrowsAsync(() => dropped.Task.WithTimeout()); + } + + public class Fixture : EventStoreClientFixture { + protected override Task Given() => + Client.CreateToStreamAsync( + Stream, + "agroupname17", + new(), + userCredentials: TestCredentials.Root + ); + + protected override Task When() => Task.CompletedTask; + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it.cs index ae2c96097..2c0f5225e 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it.cs @@ -3,62 +3,49 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; public class connect_to_existing_with_start_from_beginning_and_events_in_it : IClassFixture { - const string Group = "startinbeginning1"; + private const string Group = "startinbeginning1"; + private const string Stream = nameof(connect_to_existing_with_start_from_beginning_and_events_in_it); - const string Stream = - nameof(connect_to_existing_with_start_from_beginning_and_events_in_it); - - readonly Fixture _fixture; + private readonly Fixture _fixture; public connect_to_existing_with_start_from_beginning_and_events_in_it(Fixture fixture) => _fixture = fixture; [Fact] public async Task the_subscription_gets_event_zero_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); + var resolvedEvent = await _fixture.Subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); Assert.Equal(_fixture.Events[0].EventId, resolvedEvent.Event.EventId); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; + public readonly EventData[] Events; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(10).ToArray(); + Events = CreateTestEvents(10).ToArray(); } - public Task FirstEvent => _firstEventSource.Task; - protected override async Task Given() { await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start), - userCredentials: TestCredentials.Root - ); + await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root); + } + + protected override Task When() { + Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); + + return Task.CompletedTask; } - protected override async Task When() => - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete.cs new file mode 100644 index 000000000..9f9ced172 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete.cs @@ -0,0 +1,65 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete + : IClassFixture { + const string Group = "startinbeginning1"; + + const string Stream = + nameof(connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete); + + readonly Fixture _fixture; + + public connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task the_subscription_gets_event_zero_as_its_first_event() { + var resolvedEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); + Assert.Equal(_fixture.Events[0].EventId, resolvedEvent.Event.EventId); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstEventSource; + public readonly EventData[] Events; + PersistentSubscription? _subscription; + + public Fixture() { + _firstEventSource = new(); + Events = CreateTestEvents(10).ToArray(); + } + + public Task FirstEvent => _firstEventSource.Task; + + protected override async Task Given() { + await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); + await Client.CreateToStreamAsync( + Stream, + Group, + new(startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + } + + protected override async Task When() => + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, r, ct) => { + _firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstEventSource.TrySetException(ex!); + }, + TestCredentials.TestUser1 + ); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream.cs index 8ad3a523f..a6e9cb26b 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream.cs @@ -2,63 +2,50 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; public class connect_to_existing_with_start_from_beginning_and_no_stream : IClassFixture { - const string Group = "startinbeginning1"; + private const string Group = "startinbeginning1"; + private const string Stream = nameof(connect_to_existing_with_start_from_beginning_and_no_stream); - const string Stream = - nameof(connect_to_existing_with_start_from_beginning_and_no_stream); - - readonly Fixture _fixture; + private readonly Fixture _fixture; public connect_to_existing_with_start_from_beginning_and_no_stream(Fixture fixture) => _fixture = fixture; [Fact] public async Task the_subscription_gets_event_zero_as_its_first_event() { - var firstEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(StreamPosition.Start, firstEvent.Event.EventNumber); - Assert.Equal(_fixture.EventId, firstEvent.Event.EventId); + var resolvedEvent = await _fixture.Subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); + Assert.Equal(_fixture.EventId, resolvedEvent.Event.EventId); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; + public readonly EventData[] Events; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents().ToArray(); + Events = CreateTestEvents().ToArray(); } - public Task FirstEvent => _firstEventSource.Task; - public Uuid EventId => Events.Single().EventId; + public Uuid EventId => Events.Single().EventId; protected override async Task Given() { await Client.CreateToStreamAsync( Stream, Group, new(), - userCredentials: TestCredentials.Root - ); + userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); + Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); } protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream_obsolete.cs new file mode 100644 index 000000000..7e6cecc3b --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream_obsolete.cs @@ -0,0 +1,65 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class connect_to_existing_with_start_from_beginning_and_no_stream_obsolete + : IClassFixture { + const string Group = "startinbeginning1"; + + const string Stream = + nameof(connect_to_existing_with_start_from_beginning_and_no_stream_obsolete); + + readonly Fixture _fixture; + + public connect_to_existing_with_start_from_beginning_and_no_stream_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task the_subscription_gets_event_zero_as_its_first_event() { + var firstEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(StreamPosition.Start, firstEvent.Event.EventNumber); + Assert.Equal(_fixture.EventId, firstEvent.Event.EventId); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstEventSource; + public readonly EventData[] Events; + PersistentSubscription? _subscription; + + public Fixture() { + _firstEventSource = new(); + Events = CreateTestEvents().ToArray(); + } + + public Task FirstEvent => _firstEventSource.Task; + public Uuid EventId => Events.Single().EventId; + + protected override async Task Given() { + await Client.CreateToStreamAsync( + Stream, + Group, + new(), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, r, ct) => { + _firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstEventSource.TrySetException(ex!); + }, + TestCredentials.TestUser1 + ); + } + + protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it.cs index 1a539e47c..cef6a28a7 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it.cs @@ -1,32 +1,29 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; +[Obsolete] public class connect_to_existing_with_start_from_not_set_and_events_in_it - : IClassFixture { - const string Group = "startinbeginning1"; + : IClassFixture { + private const string Group = "startinbeginning1"; - const string Stream = - nameof(connect_to_existing_with_start_from_not_set_and_events_in_it); - - readonly Fixture _fixture; + private const string Stream = nameof(connect_to_existing_with_start_from_not_set_and_events_in_it); + private readonly Fixture _fixture; public connect_to_existing_with_start_from_not_set_and_events_in_it(Fixture fixture) => _fixture = fixture; [Fact] - public async Task the_subscription_gets_no_events() => await Assert.ThrowsAsync(() => _fixture.FirstEvent.WithTimeout()); + public async Task the_subscription_gets_no_events() => + await Assert.ThrowsAsync( + () => _fixture.Subscription!.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event) + .AsTask().WithTimeout(TimeSpan.FromMilliseconds(250))); public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; + public readonly EventData[] Events; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(10).ToArray(); + public Fixture() { + Events = CreateTestEvents(10).ToArray(); } - public Task FirstEvent => _firstEventSource.Task; - protected override async Task Given() { await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); await Client.CreateToStreamAsync( @@ -37,24 +34,19 @@ await Client.CreateToStreamAsync( ); } - protected override async Task When() => - _subscription = await Client.SubscribeToStreamAsync( + protected override Task When() { + Subscription = Client.SubscribeToStream( Stream, Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); + userCredentials: TestCredentials.TestUser1); + return Task.CompletedTask; + } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete.cs new file mode 100644 index 000000000..4baa9b373 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete.cs @@ -0,0 +1,61 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete + : IClassFixture { + const string Group = "startinbeginning1"; + + const string Stream = + nameof(connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete); + + readonly Fixture _fixture; + + public connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task the_subscription_gets_no_events() => await Assert.ThrowsAsync(() => _fixture.FirstEvent.WithTimeout()); + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstEventSource; + public readonly EventData[] Events; + PersistentSubscription? _subscription; + + public Fixture() { + _firstEventSource = new(); + Events = CreateTestEvents(10).ToArray(); + } + + public Task FirstEvent => _firstEventSource.Task; + + protected override async Task Given() { + await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); + await Client.CreateToStreamAsync( + Stream, + Group, + new(), + userCredentials: TestCredentials.Root + ); + } + + protected override async Task When() => + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, r, ct) => { + _firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstEventSource.TrySetException(ex!); + }, + TestCredentials.TestUser1 + ); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written.cs index d63d01aca..08a2172d4 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written.cs @@ -2,36 +2,34 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; public class connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written : IClassFixture { - const string Group = "startinbeginning1"; - const string Stream = nameof(connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written); + private const string Group = "startinbeginning1"; - readonly Fixture _fixture; + private const string Stream = + nameof(connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written); - public - connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written(Fixture fixture) => + private readonly Fixture _fixture; + + public connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written(Fixture fixture) => _fixture = fixture; [Fact] public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); + var resolvedEvent = await _fixture.Subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - - PersistentSubscription? _subscription; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(11).ToArray(); + Events = CreateTestEvents(11).ToArray(); } - public Task FirstEvent => _firstEventSource.Task; - protected override async Task Given() { await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); await Client.CreateToStreamAsync( @@ -41,26 +39,20 @@ await Client.CreateToStreamAsync( userCredentials: TestCredentials.Root ); - _subscription = await Client.SubscribeToStreamAsync( + Subscription = Client.SubscribeToStream( Stream, Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); + userCredentials: TestCredentials.TestUser1); } - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); + protected override Task When() => + StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete.cs new file mode 100644 index 000000000..b963c8aea --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete.cs @@ -0,0 +1,67 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete + : IClassFixture { + const string Group = "startinbeginning1"; + const string Stream = nameof(connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete); + + readonly Fixture _fixture; + + public + connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete(Fixture fixture) => + _fixture = fixture; + + [Fact] + public async Task the_subscription_gets_the_written_event_as_its_first_event() { + var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); + Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstEventSource; + + public readonly EventData[] Events; + + PersistentSubscription? _subscription; + + public Fixture() { + _firstEventSource = new(); + Events = CreateTestEvents(11).ToArray(); + } + + public Task FirstEvent => _firstEventSource.Task; + + protected override async Task Given() { + await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); + await Client.CreateToStreamAsync( + Stream, + Group, + new(), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, r, ct) => { + _firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstEventSource.TrySetException(ex!); + }, + TestCredentials.TestUser1 + ); + } + + protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it.cs index 4374fb79c..f2bf5e807 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it.cs @@ -2,30 +2,28 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; public class connect_to_existing_with_start_from_set_to_end_position_and_events_in_it : IClassFixture { - const string Group = "startinbeginning1"; + private const string Group = "startinbeginning1"; + private const string Stream = nameof(connect_to_existing_with_start_from_set_to_end_position_and_events_in_it); - const string Stream = - nameof(connect_to_existing_with_start_from_set_to_end_position_and_events_in_it); + private readonly Fixture _fixture; - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_end_position_and_events_in_it(Fixture fixture) => _fixture = fixture; + public connect_to_existing_with_start_from_set_to_end_position_and_events_in_it(Fixture fixture) => + _fixture = fixture; [Fact] - public async Task the_subscription_gets_no_events() => await Assert.ThrowsAsync(() => _fixture.FirstEvent.WithTimeout()); + public async Task the_subscription_gets_no_events() => + await Assert.ThrowsAsync( + () => _fixture.Subscription!.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event) + .AsTask().WithTimeout(TimeSpan.FromMilliseconds(250))); public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; + public readonly EventData[] Events; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(10).ToArray(); + Events = CreateTestEvents(10).ToArray(); } - public Task FirstEvent => _firstEventSource.Task; - protected override async Task Given() { await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); await Client.CreateToStreamAsync( @@ -36,24 +34,20 @@ await Client.CreateToStreamAsync( ); } - protected override async Task When() => - _subscription = await Client.SubscribeToStreamAsync( + protected override Task When() { + Subscription = Client.SubscribeToStream( Stream, Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); + userCredentials: TestCredentials.TestUser1); + return Task.CompletedTask; + } + + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete.cs new file mode 100644 index 000000000..53a6d35d0 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete.cs @@ -0,0 +1,60 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete + : IClassFixture { + const string Group = "startinbeginning1"; + + const string Stream = + nameof(connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete); + + readonly Fixture _fixture; + + public connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task the_subscription_gets_no_events() => await Assert.ThrowsAsync(() => _fixture.FirstEvent.WithTimeout()); + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstEventSource; + public readonly EventData[] Events; + PersistentSubscription? _subscription; + + public Fixture() { + _firstEventSource = new(); + Events = CreateTestEvents(10).ToArray(); + } + + public Task FirstEvent => _firstEventSource.Task; + + protected override async Task Given() { + await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); + await Client.CreateToStreamAsync( + Stream, + Group, + new(startFrom: StreamPosition.End), + userCredentials: TestCredentials.Root + ); + } + + protected override async Task When() => + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, r, ct) => { + _firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstEventSource.TrySetException(ex!); + }, + TestCredentials.TestUser1 + ); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written.cs index eace58056..2e7b8a4c1 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written.cs @@ -1,18 +1,14 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; -public class - connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written - : IClassFixture< - connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written - .Fixture> { - const string Group = "startinbeginning1"; +public class connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written + : IClassFixture { + private const string Group = "startinbeginning1"; - const string Stream = - nameof( - connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written - ); + private const string Stream = + nameof(connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written); - readonly Fixture _fixture; + private readonly Fixture _fixture; public connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written(Fixture fixture) => @@ -20,52 +16,42 @@ public class [Fact] public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); + var resolvedEvent = await _fixture.Subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; + public readonly EventData[] Events; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(11).ToArray(); + Events = CreateTestEvents(11).ToArray(); } - public Task FirstEvent => _firstEventSource.Task; - protected override async Task Given() { await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); await Client.CreateToStreamAsync( Stream, Group, new(startFrom: StreamPosition.End), - userCredentials: TestCredentials.Root - ); + userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); + Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); } - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); + protected override Task When() => + StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); + + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete.cs new file mode 100644 index 000000000..dd075613e --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete.cs @@ -0,0 +1,72 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class + connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete + : IClassFixture< + connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete + .Fixture> { + const string Group = "startinbeginning1"; + + const string Stream = + nameof( + connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete + ); + + readonly Fixture _fixture; + + public + connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete(Fixture fixture) => + _fixture = fixture; + + [Fact] + public async Task the_subscription_gets_the_written_event_as_its_first_event() { + var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); + Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstEventSource; + public readonly EventData[] Events; + PersistentSubscription? _subscription; + + public Fixture() { + _firstEventSource = new(); + Events = CreateTestEvents(11).ToArray(); + } + + public Task FirstEvent => _firstEventSource.Task; + + protected override async Task Given() { + await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); + await Client.CreateToStreamAsync( + Stream, + Group, + new(startFrom: StreamPosition.End), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, r, ct) => { + _firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstEventSource.TrySetException(ex!); + }, + TestCredentials.TestUser1 + ); + } + + protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream.cs index 6c12310ac..0199eaa3b 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream.cs @@ -2,63 +2,54 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; public class connect_to_existing_with_start_from_two_and_no_stream : IClassFixture { - const string Group = "startinbeginning1"; + private const string Group = "startinbeginning1"; + private const string Stream = nameof(connect_to_existing_with_start_from_two_and_no_stream); - const string Stream = - nameof(connect_to_existing_with_start_from_two_and_no_stream); - - readonly Fixture _fixture; + private readonly Fixture _fixture; public connect_to_existing_with_start_from_two_and_no_stream(Fixture fixture) => _fixture = fixture; [Fact] public async Task the_subscription_gets_event_two_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); + var resolvedEvent = await _fixture.Subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + Assert.Equal(new(2), resolvedEvent.Event.EventNumber); Assert.Equal(_fixture.EventId, resolvedEvent.Event.EventId); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; + public readonly EventData[] Events; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(3).ToArray(); + Events = CreateTestEvents(3).ToArray(); } - public Task FirstEvent => _firstEventSource.Task; - public Uuid EventId => Events.Last().EventId; + public Uuid EventId => Events.Last().EventId; protected override async Task Given() { await Client.CreateToStreamAsync( Stream, Group, new(startFrom: new StreamPosition(2)), - userCredentials: TestCredentials.Root - ); + userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToStreamAsync( + Subscription = Client.SubscribeToStream( Stream, Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); + userCredentials: TestCredentials.TestUser1); } protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream_obsolete.cs new file mode 100644 index 000000000..5e172e0bd --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream_obsolete.cs @@ -0,0 +1,65 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class connect_to_existing_with_start_from_two_and_no_stream_obsolete + : IClassFixture { + const string Group = "startinbeginning1"; + + const string Stream = + nameof(connect_to_existing_with_start_from_two_and_no_stream_obsolete); + + readonly Fixture _fixture; + + public connect_to_existing_with_start_from_two_and_no_stream_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task the_subscription_gets_event_two_as_its_first_event() { + var resolvedEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(new(2), resolvedEvent.Event.EventNumber); + Assert.Equal(_fixture.EventId, resolvedEvent.Event.EventId); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstEventSource; + public readonly EventData[] Events; + PersistentSubscription? _subscription; + + public Fixture() { + _firstEventSource = new(); + Events = CreateTestEvents(3).ToArray(); + } + + public Task FirstEvent => _firstEventSource.Task; + public Uuid EventId => Events.Last().EventId; + + protected override async Task Given() { + await Client.CreateToStreamAsync( + Stream, + Group, + new(startFrom: new StreamPosition(2)), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, r, ct) => { + _firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstEventSource.TrySetException(ex!); + }, + TestCredentials.TestUser1 + ); + } + + protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it.cs index edaed2e2f..3a7edafba 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it.cs @@ -2,63 +2,54 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; public class connect_to_existing_with_start_from_x_set_and_events_in_it : IClassFixture { - const string Group = "startinx2"; + private const string Group = "startinx2"; + private const string Stream = nameof(connect_to_existing_with_start_from_x_set_and_events_in_it); - const string Stream = - nameof(connect_to_existing_with_start_from_x_set_and_events_in_it); - - readonly Fixture _fixture; + private readonly Fixture _fixture; public connect_to_existing_with_start_from_x_set_and_events_in_it(Fixture fixture) => _fixture = fixture; [Fact] public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); + var resolvedEvent = await _fixture.Subscription!.Messages + .OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); Assert.Equal(new(4), resolvedEvent.Event.EventNumber); Assert.Equal(_fixture.Events.Skip(4).First().EventId, resolvedEvent.Event.EventId); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; + public readonly EventData[] Events; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(10).ToArray(); + Events = CreateTestEvents(10).ToArray(); } - public Task FirstEvent => _firstEventSource.Task; - protected override async Task Given() { await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); await Client.CreateToStreamAsync( Stream, Group, new(startFrom: new StreamPosition(4)), - userCredentials: TestCredentials.Root - ); + userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToStreamAsync( + Subscription = Client.SubscribeToStream( Stream, Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); + userCredentials: TestCredentials.TestUser1); } - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); + protected override Task When() => + StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); + + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete.cs new file mode 100644 index 000000000..df18f52f5 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete.cs @@ -0,0 +1,65 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete + : IClassFixture { + const string Group = "startinx2"; + + const string Stream = + nameof(connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete); + + readonly Fixture _fixture; + + public connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task the_subscription_gets_the_written_event_as_its_first_event() { + var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); + Assert.Equal(new(4), resolvedEvent.Event.EventNumber); + Assert.Equal(_fixture.Events.Skip(4).First().EventId, resolvedEvent.Event.EventId); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstEventSource; + public readonly EventData[] Events; + PersistentSubscription? _subscription; + + public Fixture() { + _firstEventSource = new(); + Events = CreateTestEvents(10).ToArray(); + } + + public Task FirstEvent => _firstEventSource.Task; + + protected override async Task Given() { + await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); + await Client.CreateToStreamAsync( + Stream, + Group, + new(startFrom: new StreamPosition(4)), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, r, ct) => { + _firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstEventSource.TrySetException(ex!); + }, + TestCredentials.TestUser1 + ); + } + + protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written.cs index e29033175..276bae9aa 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written.cs @@ -1,67 +1,54 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; public class connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written - : IClassFixture< - connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written. - Fixture> { - const string Group = "startinbeginning1"; + : IClassFixture { + private const string Group = "startinbeginning1"; + private const string Stream = nameof(connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written); + private readonly Fixture _fixture; - const string Stream = - nameof(connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written - ); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written(Fixture fixture) => _fixture = fixture; + public connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written(Fixture fixture) => + _fixture = fixture; [Fact] public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); + var resolvedEvent = await _fixture.Subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); Assert.Equal(new(10), resolvedEvent.Event.EventNumber); Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; + public readonly EventData[] Events; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(11).ToArray(); + Events = CreateTestEvents(11).ToArray(); } - public Task FirstEvent => _firstEventSource.Task; - protected override async Task Given() { await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); await Client.CreateToStreamAsync( Stream, Group, new(startFrom: new StreamPosition(10)), - userCredentials: TestCredentials.Root - ); + userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToStreamAsync( + Subscription = Client.SubscribeToStream( Stream, Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); + userCredentials: TestCredentials.TestUser1); } - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); + protected override Task When() => + StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); + + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete.cs new file mode 100644 index 000000000..82eb4e16d --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete.cs @@ -0,0 +1,68 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete + : IClassFixture< + connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete. + Fixture> { + const string Group = "startinbeginning1"; + + const string Stream = + nameof(connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete + ); + + readonly Fixture _fixture; + + public connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task the_subscription_gets_the_written_event_as_its_first_event() { + var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); + Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstEventSource; + public readonly EventData[] Events; + PersistentSubscription? _subscription; + + public Fixture() { + _firstEventSource = new(); + Events = CreateTestEvents(11).ToArray(); + } + + public Task FirstEvent => _firstEventSource.Task; + + protected override async Task Given() { + await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); + await Client.CreateToStreamAsync( + Stream, + Group, + new(startFrom: new StreamPosition(10)), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, r, ct) => { + _firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstEventSource.TrySetException(ex!); + }, + TestCredentials.TestUser1 + ); + } + + protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written.cs index 6f91cc8cc..f4b249372 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written.cs @@ -1,18 +1,13 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; -public class - connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written - : IClassFixture< - connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written - .Fixture> { - const string Group = "startinbeginning1"; +public class connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written : + IClassFixture { + private const string Group = "startinbeginning1"; + private const string Stream = + nameof(connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written); - const string Stream = - nameof( - connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written - ); - - readonly Fixture _fixture; + private readonly Fixture _fixture; public connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written(Fixture fixture) => @@ -20,52 +15,44 @@ public class [Fact] public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); + var resolvedEvent = await _fixture.Subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); Assert.Equal(new(11), resolvedEvent.Event.EventNumber); Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; + public readonly EventData[] Events; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(12).ToArray(); + Events = CreateTestEvents(12).ToArray(); } - public Task FirstEvent => _firstEventSource.Task; - protected override async Task Given() { await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(11)); await Client.CreateToStreamAsync( Stream, Group, new(startFrom: new StreamPosition(11)), - userCredentials: TestCredentials.Root - ); + userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToStreamAsync( + Subscription = Client.SubscribeToStream( Stream, Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); + userCredentials: TestCredentials.TestUser1); } - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(10), Events.Skip(11)); + protected override Task When() => + StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(10), Events.Skip(11)); + + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete.cs new file mode 100644 index 000000000..c6ca83268 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete.cs @@ -0,0 +1,72 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class + connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete + : IClassFixture< + connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete + .Fixture> { + const string Group = "startinbeginning1"; + + const string Stream = + nameof( + connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete + ); + + readonly Fixture _fixture; + + public + connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete(Fixture fixture) => + _fixture = fixture; + + [Fact] + public async Task the_subscription_gets_the_written_event_as_its_first_event() { + var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); + Assert.Equal(new(11), resolvedEvent.Event.EventNumber); + Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstEventSource; + public readonly EventData[] Events; + PersistentSubscription? _subscription; + + public Fixture() { + _firstEventSource = new(); + Events = CreateTestEvents(12).ToArray(); + } + + public Task FirstEvent => _firstEventSource.Task; + + protected override async Task Given() { + await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(11)); + await Client.CreateToStreamAsync( + Stream, + Group, + new(startFrom: new StreamPosition(11)), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, r, ct) => { + _firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstEventSource.TrySetException(ex!); + }, + TestCredentials.TestUser1 + ); + } + + protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(10), Events.Skip(11)); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions.cs index 30c2453ed..756de2519 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions.cs @@ -1,22 +1,17 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; -public class connect_to_existing_without_permissions - : IClassFixture { - const string Stream = "$" + nameof(connect_to_existing_without_permissions); - readonly Fixture _fixture; +public class connect_to_existing_without_permissions : IClassFixture { + private const string Stream = $"${nameof(connect_to_existing_without_permissions)}"; + private readonly Fixture _fixture; public connect_to_existing_without_permissions(Fixture fixture) => _fixture = fixture; [Fact] public Task throws_access_denied() => Assert.ThrowsAsync( async () => { - using var _ = await _fixture.Client.SubscribeToStreamAsync( - Stream, - "agroupname55", - delegate { return Task.CompletedTask; } - ); - } - ).WithTimeout(); + await using var subscription = _fixture.Client.SubscribeToStream(Stream, "agroupname55"); + await subscription.Messages.AnyAsync(); + }).WithTimeout(); public class Fixture : EventStoreClientFixture { public Fixture() : base(noDefaultCredentials: true) { } @@ -31,4 +26,4 @@ protected override Task Given() => protected override Task When() => Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions_obsolete.cs new file mode 100644 index 000000000..fd2249786 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions_obsolete.cs @@ -0,0 +1,35 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class connect_to_existing_without_permissions_obsolete + : IClassFixture { + const string Stream = "$" + nameof(connect_to_existing_without_permissions_obsolete); + readonly Fixture _fixture; + public connect_to_existing_without_permissions_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public Task throws_access_denied() => + Assert.ThrowsAsync( + async () => { + using var _ = await _fixture.Client.SubscribeToStreamAsync( + Stream, + "agroupname55", + delegate { return Task.CompletedTask; } + ); + } + ).WithTimeout(); + + public class Fixture : EventStoreClientFixture { + public Fixture() : base(noDefaultCredentials: true) { } + + protected override Task Given() => + Client.CreateToStreamAsync( + Stream, + "agroupname55", + new(), + userCredentials: TestCredentials.Root + ); + + protected override Task When() => Task.CompletedTask; + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions.cs index 71155a116..3e13c3983 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions.cs @@ -1,33 +1,24 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; -public class connect_to_non_existing_with_permissions - : IClassFixture { - const string Stream = nameof(connect_to_non_existing_with_permissions); - const string Group = "foo"; +public class connect_to_non_existing_with_permissions : IClassFixture { + private const string Stream = nameof(connect_to_non_existing_with_permissions); + private const string Group = "foo"; - readonly Fixture _fixture; + private readonly Fixture _fixture; public connect_to_non_existing_with_permissions(Fixture fixture) => _fixture = fixture; [Fact] public async Task throws_persistent_subscription_not_found() { - var ex = await Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToStreamAsync( - Stream, - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ); - } - ).WithTimeout(); - - Assert.Equal(Stream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); + await using var subscription = _fixture.Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); + Assert.True(await subscription.Messages.OfType() + .AnyAsync() + .AsTask() + .WithTimeout()); } public class Fixture : EventStoreClientFixture { protected override Task Given() => Task.CompletedTask; protected override Task When() => Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions_obsolete.cs new file mode 100644 index 000000000..afbb3abe3 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions_obsolete.cs @@ -0,0 +1,34 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class connect_to_non_existing_with_permissions_obsolete + : IClassFixture { + const string Stream = nameof(connect_to_non_existing_with_permissions_obsolete); + const string Group = "foo"; + + readonly Fixture _fixture; + + public connect_to_non_existing_with_permissions_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task throws_persistent_subscription_not_found() { + var ex = await Assert.ThrowsAsync( + async () => { + using var _ = await _fixture.Client.SubscribeToStreamAsync( + Stream, + Group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ); + } + ).WithTimeout(); + + Assert.Equal(Stream, ex.StreamName); + Assert.Equal(Group, ex.GroupName); + } + + public class Fixture : EventStoreClientFixture { + protected override Task Given() => Task.CompletedTask; + protected override Task When() => Task.CompletedTask; + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries.cs index 5a57acc78..89995fedf 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries.cs @@ -1,70 +1,60 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; -public class connect_with_retries - : IClassFixture { - const string Group = "retries"; - const string Stream = nameof(connect_with_retries); +public class connect_with_retries : IClassFixture { + private const string Group = "retries"; + private const string Stream = nameof(connect_with_retries); - readonly Fixture _fixture; + private readonly Fixture _fixture; public connect_with_retries(Fixture fixture) => _fixture = fixture; [Fact] - public async Task events_are_retried_until_success() => Assert.Equal(5, await _fixture.RetryCount.WithTimeout()); + public async Task events_are_retried_until_success() { + var retryCount = await _fixture.Subscription!.Messages.OfType() + .SelectAwait(async e => { + if (e.RetryCount > 4) { + await _fixture.Subscription.Ack(e.ResolvedEvent); + } else { + await _fixture.Subscription.Nack(PersistentSubscriptionNakEventAction.Retry, + "Not yet tried enough times", e.ResolvedEvent); + } + + return e.RetryCount; + }) + .Where(retryCount => retryCount > 4) + .FirstOrDefaultAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(5, retryCount); + } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _retryCountSource; public readonly EventData[] Events; - PersistentSubscription? _subscription; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _retryCountSource = new(); - Events = CreateTestEvents().ToArray(); } - public Task RetryCount => _retryCountSource.Task; - protected override async Task Given() { await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start), - userCredentials: TestCredentials.Root - ); + await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - if (r > 4) { - _retryCountSource.TrySetResult(r.Value); - await subscription.Ack(e.Event.EventId); - } - else { - await subscription.Nack( - PersistentSubscriptionNakEventAction.Retry, - "Not yet tried enough times", - e - ); - } - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _retryCountSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); + Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); } protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries_obsolete.cs new file mode 100644 index 000000000..7e0cdcf2d --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries_obsolete.cs @@ -0,0 +1,70 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class connect_with_retries_obsolete : IClassFixture { + const string Group = "retries"; + const string Stream = nameof(connect_with_retries_obsolete); + + readonly Fixture _fixture; + + public connect_with_retries_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task events_are_retried_until_success() => Assert.Equal(5, await _fixture.RetryCount.WithTimeout()); + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _retryCountSource; + + public readonly EventData[] Events; + + PersistentSubscription? _subscription; + + public Fixture() { + _retryCountSource = new(); + + Events = CreateTestEvents().ToArray(); + } + + public Task RetryCount => _retryCountSource.Task; + + protected override async Task Given() { + await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); + await Client.CreateToStreamAsync( + Stream, + Group, + new(startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, r, ct) => { + if (r > 4) { + _retryCountSource.TrySetResult(r.Value); + await subscription.Ack(e.Event.EventId); + } + else { + await subscription.Nack( + PersistentSubscriptionNakEventAction.Retry, + "Not yet tried enough times", + e + ); + } + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _retryCountSource.TrySetException(ex!); + }, + TestCredentials.TestUser1 + ); + } + + protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription.cs index e231ae077..a9dbb4b59 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription.cs @@ -1,71 +1,47 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; -public class - connecting_to_a_persistent_subscription - : IClassFixture< - connecting_to_a_persistent_subscription - .Fixture> { - const string Group = "startinbeginning1"; +public class connecting_to_a_persistent_subscription : IClassFixture { + private const string Group = "startinbeginning1"; + private const string Stream = nameof(connecting_to_a_persistent_subscription); - const string Stream = - nameof( - connecting_to_a_persistent_subscription - ); + private readonly Fixture _fixture; - readonly Fixture _fixture; - - public - connecting_to_a_persistent_subscription(Fixture fixture) => - _fixture = fixture; + public connecting_to_a_persistent_subscription(Fixture fixture) => _fixture = fixture; [Fact] public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); + var resolvedEvent = await _fixture.Subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); Assert.Equal(new(11), resolvedEvent.Event.EventNumber); Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; + public readonly EventData[] Events; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(12).ToArray(); + Events = CreateTestEvents(12).ToArray(); } - public Task FirstEvent => _firstEventSource.Task; - protected override async Task Given() { await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(11)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(11)), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); + await Client.CreateToStreamAsync(Stream, Group, new(startFrom: new StreamPosition(11)), + userCredentials: TestCredentials.Root); + + Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); } - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(10), Events.Skip(11)); + protected override Task When() => + StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(10), Events.Skip(11)); + + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription_obsolete.cs new file mode 100644 index 000000000..c8f18ce69 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription_obsolete.cs @@ -0,0 +1,72 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class + connecting_to_a_persistent_subscription_obsolete + : IClassFixture< + connecting_to_a_persistent_subscription_obsolete + .Fixture> { + const string Group = "startinbeginning1"; + + const string Stream = + nameof( + connecting_to_a_persistent_subscription_obsolete + ); + + readonly Fixture _fixture; + + public + connecting_to_a_persistent_subscription_obsolete(Fixture fixture) => + _fixture = fixture; + + [Fact] + public async Task the_subscription_gets_the_written_event_as_its_first_event() { + var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); + Assert.Equal(new(11), resolvedEvent.Event.EventNumber); + Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _firstEventSource; + public readonly EventData[] Events; + PersistentSubscription? _subscription; + + public Fixture() { + _firstEventSource = new(); + Events = CreateTestEvents(12).ToArray(); + } + + public Task FirstEvent => _firstEventSource.Task; + + protected override async Task Given() { + await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(11)); + await Client.CreateToStreamAsync( + Stream, + Group, + new(startFrom: new StreamPosition(11)), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, r, ct) => { + _firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + _firstEventSource.TrySetException(ex!); + }, + TestCredentials.TestUser1 + ); + } + + protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(10), Events.Skip(11)); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs index cb268b603..dae83cec6 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs @@ -1,68 +1,26 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; -public class deleting_existing_with_subscriber - : IClassFixture { - const string Stream = nameof(deleting_existing_with_subscriber); - readonly Fixture _fixture; +public class deleting_existing_with_subscriber : IClassFixture { + private const string Stream = nameof(deleting_existing_with_subscriber); + private readonly Fixture _fixture; public deleting_existing_with_subscriber(Fixture fixture) => _fixture = fixture; [Fact] - public async Task the_subscription_is_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - -#if NET - Assert.Equal(Stream, ex.StreamName); - Assert.Equal("groupname123", ex.GroupName); -#endif - } - - [Fact(Skip = "Isn't this how it should work?")] public async Task the_subscription_is_dropped_with_not_found() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - Assert.Equal(Stream, ex.StreamName); - Assert.Equal("groupname123", ex.GroupName); + await using var subscription = _fixture.Client.SubscribeToStream(Stream, "groupname123", userCredentials: TestCredentials.Root); + + Assert.True(await subscription.Messages.OfType().AnyAsync() + .AsTask() + .WithTimeout()); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped; - PersistentSubscription? _subscription; - - public Fixture() => _dropped = new(); - - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task; - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - "groupname123", - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - "groupname123", - (_, _, _, _) => Task.CompletedTask, - (_, r, e) => _dropped.TrySetResult((r, e)), - TestCredentials.Root - ); + await Client.CreateToStreamAsync(Stream, "groupname123", new(), userCredentials: TestCredentials.Root); } protected override Task When() => - Client.DeleteToStreamAsync( - Stream, - "groupname123", - userCredentials: TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } + Client.DeleteToStreamAsync(Stream, "groupname123", userCredentials: TestCredentials.Root); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber_obsolete.cs new file mode 100644 index 000000000..3a6a5433d --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber_obsolete.cs @@ -0,0 +1,67 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class deleting_existing_with_subscriber_obsolete + : IClassFixture { + const string Stream = nameof(deleting_existing_with_subscriber_obsolete); + readonly Fixture _fixture; + + public deleting_existing_with_subscriber_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task the_subscription_is_dropped() { + var (reason, exception) = await _fixture.Dropped.WithTimeout(); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + + Assert.Equal(Stream, ex.StreamName); + Assert.Equal("groupname123", ex.GroupName); + } + + [Fact(Skip = "Isn't this how it should work?")] + public async Task the_subscription_is_dropped_with_not_found() { + var (reason, exception) = await _fixture.Dropped.WithTimeout(); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + Assert.Equal(Stream, ex.StreamName); + Assert.Equal("groupname123", ex.GroupName); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped; + PersistentSubscription? _subscription; + + public Fixture() => _dropped = new(); + + public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task; + + protected override async Task Given() { + await Client.CreateToStreamAsync( + Stream, + "groupname123", + new(), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + "groupname123", + (_, _, _, _) => Task.CompletedTask, + (_, r, e) => _dropped.TrySetResult((r, e)), + TestCredentials.Root + ); + } + + protected override Task When() => + Client.DeleteToStreamAsync( + Stream, + "groupname123", + userCredentials: TestCredentials.Root + ); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info.cs index baa1b0fad..bec3c4118 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info.cs @@ -1,10 +1,10 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; public class get_info : IClassFixture { - const string GroupName = nameof(get_info); - const string StreamName = nameof(get_info); + private const string GroupName = nameof(get_info); + private const string StreamName = nameof(get_info); - static readonly PersistentSubscriptionSettings _settings = new( + private static readonly PersistentSubscriptionSettings _settings = new( true, StreamPosition.Start, true, @@ -20,7 +20,7 @@ public class get_info : IClassFixture { SystemConsumerStrategies.RoundRobin ); - readonly Fixture _fixture; + private readonly Fixture _fixture; public get_info(Fixture fixture) => _fixture = fixture; @@ -32,11 +32,7 @@ public static IEnumerable AllowedUsers() { [Theory] [MemberData(nameof(AllowedUsers))] public async Task returns_expected_result(UserCredentials credentials) { - var result = await _fixture.Client.GetInfoToStreamAsync( - StreamName, - GroupName, - userCredentials: credentials - ); + var result = await _fixture.Client.GetInfoToStreamAsync(StreamName, GroupName, userCredentials: credentials); Assert.Equal(StreamName, result.EventSource); Assert.Equal(GroupName, result.GroupName); @@ -145,54 +141,58 @@ public async Task returns_result_for_normal_user() { Assert.NotNull(result); } - void AssertKeyAndValue(IDictionary items, string key) { + private void AssertKeyAndValue(IDictionary items, string key) { Assert.True(items.ContainsKey(key)); Assert.True(items[key] > 0); } public class Fixture : EventStoreClientFixture { + private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; + private IAsyncEnumerator? _enumerator; public Fixture() : base(noDefaultCredentials: true) { } protected override Task Given() => - Client.CreateToStreamAsync( - groupName: GroupName, - streamName: StreamName, - settings: _settings, - userCredentials: TestCredentials.Root - ); + Client.CreateToStreamAsync(groupName: GroupName, streamName: StreamName, settings: _settings, + userCredentials: TestCredentials.Root); protected override async Task When() { var counter = 0; - var tcs = new TaskCompletionSource(); - - await Client.SubscribeToStreamAsync( - StreamName, - GroupName, - (s, e, r, ct) => { - counter++; - - if (counter == 1) - s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e); - - if (counter > 10) - tcs.TrySetResult(); - - return Task.CompletedTask; - }, - userCredentials: TestCredentials.Root - ); - - for (var i = 0; i < 15; i++) - await StreamsClient.AppendToStreamAsync( - StreamName, - StreamState.Any, - new[] { - new EventData(Uuid.NewUuid(), "test-event", ReadOnlyMemory.Empty) - }, - userCredentials: TestCredentials.Root - ); + _subscription = Client.SubscribeToStream(StreamName, GroupName, userCredentials: TestCredentials.Root); + _enumerator = _subscription.Messages.GetAsyncEnumerator(); + + for (var i = 0; i < 15; i++) { + await StreamsClient.AppendToStreamAsync(StreamName, StreamState.Any, + new[] { new EventData(Uuid.NewUuid(), "test-event", ReadOnlyMemory.Empty) }, + userCredentials: TestCredentials.Root); + } + + while (await _enumerator.MoveNextAsync()) { + if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + counter++; + + if (counter == 1) { + await _subscription.Nack(PersistentSubscriptionNakEventAction.Park, "Test", resolvedEvent); + } + + if (counter > 10) { + return; + } + } + } + + public override async Task DisposeAsync() { + if (_enumerator is not null) { + await _enumerator.DisposeAsync(); + } + + if (_subscription is not null) { + await _subscription.DisposeAsync(); + } - await tcs.Task; + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info_obsolete.cs new file mode 100644 index 000000000..6d322d699 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info_obsolete.cs @@ -0,0 +1,199 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class get_info_obsolete : IClassFixture { + const string GroupName = nameof(get_info_obsolete); + const string StreamName = nameof(get_info_obsolete); + + static readonly PersistentSubscriptionSettings _settings = new( + true, + StreamPosition.Start, + true, + TimeSpan.FromSeconds(9), + 11, + 303, + 30, + 909, + TimeSpan.FromSeconds(1), + 1, + 1, + 500, + SystemConsumerStrategies.RoundRobin + ); + + readonly Fixture _fixture; + + public get_info_obsolete(Fixture fixture) => _fixture = fixture; + + public static IEnumerable AllowedUsers() { + yield return new object[] { TestCredentials.Root }; + yield return new object[] { TestCredentials.TestUser1 }; + } + + [Theory] + [MemberData(nameof(AllowedUsers))] + public async Task returns_expected_result(UserCredentials credentials) { + var result = await _fixture.Client.GetInfoToStreamAsync( + StreamName, + GroupName, + userCredentials: credentials + ); + + Assert.Equal(StreamName, result.EventSource); + Assert.Equal(GroupName, result.GroupName); + Assert.NotNull(_settings.StartFrom); + Assert.True(result.Stats.TotalItems > 0); + Assert.True(result.Stats.OutstandingMessagesCount > 0); + Assert.True(result.Stats.AveragePerSecond >= 0); + Assert.True(result.Stats.ParkedMessageCount >= 0); + Assert.True(result.Stats.AveragePerSecond >= 0); + Assert.True(result.Stats.ReadBufferCount >= 0); + Assert.True(result.Stats.RetryBufferCount >= 0); + Assert.True(result.Stats.CountSinceLastMeasurement >= 0); + Assert.True(result.Stats.TotalInFlightMessages >= 0); + Assert.NotNull(result.Stats.LastKnownEventPosition); + Assert.NotNull(result.Stats.LastCheckpointedEventPosition); + Assert.True(result.Stats.LiveBufferCount >= 0); + + Assert.NotNull(result.Connections); + Assert.NotEmpty(result.Connections); + var connection = result.Connections.First(); + Assert.NotNull(connection.From); + Assert.Equal(TestCredentials.Root.Username, connection.Username); + Assert.NotEmpty(connection.ConnectionName); + Assert.True(connection.AverageItemsPerSecond >= 0); + Assert.True(connection.TotalItems >= 0); + Assert.True(connection.CountSinceLastMeasurement >= 0); + Assert.True(connection.AvailableSlots >= 0); + Assert.True(connection.InFlightMessages >= 0); + Assert.NotNull(connection.ExtraStatistics); + Assert.NotEmpty(connection.ExtraStatistics); + + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Highest); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Mean); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Median); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Fastest); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile1); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile2); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile3); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile4); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile5); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyPercent); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyFivePercent); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePercent); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointFivePercent); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointNinePercent); + + Assert.NotNull(result.Settings); + Assert.Equal(_settings.StartFrom, result.Settings!.StartFrom); + Assert.Equal(_settings.ResolveLinkTos, result.Settings!.ResolveLinkTos); + Assert.Equal(_settings.ExtraStatistics, result.Settings!.ExtraStatistics); + Assert.Equal(_settings.MessageTimeout, result.Settings!.MessageTimeout); + Assert.Equal(_settings.MaxRetryCount, result.Settings!.MaxRetryCount); + Assert.Equal(_settings.LiveBufferSize, result.Settings!.LiveBufferSize); + Assert.Equal(_settings.ReadBatchSize, result.Settings!.ReadBatchSize); + Assert.Equal(_settings.HistoryBufferSize, result.Settings!.HistoryBufferSize); + Assert.Equal(_settings.CheckPointAfter, result.Settings!.CheckPointAfter); + Assert.Equal(_settings.CheckPointLowerBound, result.Settings!.CheckPointLowerBound); + Assert.Equal(_settings.CheckPointUpperBound, result.Settings!.CheckPointUpperBound); + Assert.Equal(_settings.MaxSubscriberCount, result.Settings!.MaxSubscriberCount); + Assert.Equal(_settings.ConsumerStrategyName, result.Settings!.ConsumerStrategyName); + } + + [Fact] + public async Task throws_when_given_non_existing_subscription() => + await Assert.ThrowsAsync( + async () => { + await _fixture.Client.GetInfoToStreamAsync( + "NonExisting", + "NonExisting", + userCredentials: TestCredentials.Root + ); + } + ); + + [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] + public async Task throws_with_non_existing_user() => + await Assert.ThrowsAsync( + async () => { + await _fixture.Client.GetInfoToStreamAsync( + "NonExisting", + "NonExisting", + userCredentials: TestCredentials.TestBadUser + ); + } + ); + + [Fact] + public async Task throws_with_no_credentials() => + await Assert.ThrowsAsync( + async () => { + await _fixture.Client.GetInfoToStreamAsync( + "NonExisting", + "NonExisting" + ); + } + ); + + [Fact] + public async Task returns_result_for_normal_user() { + var result = await _fixture.Client.GetInfoToStreamAsync( + StreamName, + GroupName, + userCredentials: TestCredentials.TestUser1 + ); + + Assert.NotNull(result); + } + + void AssertKeyAndValue(IDictionary items, string key) { + Assert.True(items.ContainsKey(key)); + Assert.True(items[key] > 0); + } + + public class Fixture : EventStoreClientFixture { + public Fixture() : base(noDefaultCredentials: true) { } + + protected override Task Given() => + Client.CreateToStreamAsync( + groupName: GroupName, + streamName: StreamName, + settings: _settings, + userCredentials: TestCredentials.Root + ); + + protected override async Task When() { + var counter = 0; + var tcs = new TaskCompletionSource(); + + await Client.SubscribeToStreamAsync( + StreamName, + GroupName, + (s, e, r, ct) => { + counter++; + + if (counter == 1) + s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e); + + if (counter > 10) + tcs.TrySetResult(); + + return Task.CompletedTask; + }, + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < 15; i++) + await StreamsClient.AppendToStreamAsync( + StreamName, + StreamState.Any, + new[] { + new EventData(Uuid.NewUuid(), "test-event", ReadOnlyMemory.Empty) + }, + userCredentials: TestCredentials.Root + ); + + await tcs.Task; + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack.cs index a692b74c7..864eccd8a 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack.cs @@ -4,76 +4,56 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; public class happy_case_catching_up_to_link_to_events_manual_ack : IClassFixture { - const string Stream = nameof(happy_case_catching_up_to_link_to_events_manual_ack); - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; + private const string Stream = nameof(happy_case_catching_up_to_link_to_events_manual_ack); + private const string Group = nameof(Group); + private const int BufferCount = 10; + private const int EventWriteCount = BufferCount * 2; - readonly Fixture _fixture; + private readonly Fixture _fixture; public happy_case_catching_up_to_link_to_events_manual_ack(Fixture fixture) => _fixture = fixture; [Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + public async Task Test() { + await _fixture.Subscription!.Messages.OfType() + .Take(_fixture.Events.Length) + .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + } public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; + public readonly EventData[] Events; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _events = CreateTestEvents(EventWriteCount) - .Select( - (e, i) => new EventData( - e.EventId, - SystemEventTypes.LinkTo, - Encoding.UTF8.GetBytes($"{i}@{Stream}"), - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream - ) - ) - .ToArray(); - - _eventsReceived = new(); + Events = CreateTestEvents(EventWriteCount) + .Select((e, i) => new EventData( + e.EventId, + SystemEventTypes.LinkTo, + Encoding.UTF8.GetBytes($"{i}@{Stream}"), + contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream + )).ToArray(); } - public Task EventsReceived => _eventsReceived.Task; - protected override async Task Given() { - foreach (var e in _events) + foreach (var e in Events) await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); + await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root); - if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); + Subscription = Client.SubscribeToStream(Stream, Group, bufferSize: BufferCount, + userCredentials: TestCredentials.Root); } protected override Task When() => Task.CompletedTask; - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs new file mode 100644 index 000000000..b1cb84438 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs @@ -0,0 +1,80 @@ +using System.Text; + +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class happy_case_catching_up_to_link_to_events_manual_ack_obsolete + : IClassFixture { + const string Stream = nameof(happy_case_catching_up_to_link_to_events_manual_ack_obsolete); + const string Group = nameof(Group); + const int BufferCount = 10; + const int EventWriteCount = BufferCount * 2; + + readonly Fixture _fixture; + + public happy_case_catching_up_to_link_to_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + + public class Fixture : EventStoreClientFixture { + readonly EventData[] _events; + readonly TaskCompletionSource _eventsReceived; + int _eventReceivedCount; + + PersistentSubscription? _subscription; + + public Fixture() { + _events = CreateTestEvents(EventWriteCount) + .Select( + (e, i) => new EventData( + e.EventId, + SystemEventTypes.LinkTo, + Encoding.UTF8.GetBytes($"{i}@{Stream}"), + contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream + ) + ) + .ToArray(); + + _eventsReceived = new(); + } + + public Task EventsReceived => _eventsReceived.Task; + + protected override async Task Given() { + foreach (var e in _events) + await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); + + await Client.CreateToStreamAsync( + Stream, + Group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + + if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) + _eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + _eventsReceived.TrySetException(e); + }, + bufferSize: BufferCount, + userCredentials: TestCredentials.Root + ); + } + + protected override Task When() => Task.CompletedTask; + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack.cs index 7085b5a81..5bcf18171 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack.cs @@ -1,66 +1,53 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; -public class happy_case_catching_up_to_normal_events_manual_ack : IClassFixture { - const string Stream = nameof(happy_case_catching_up_to_normal_events_manual_ack); - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; +public class happy_case_catching_up_to_normal_events_manual_ack + : IClassFixture { + private const string Stream = nameof(happy_case_catching_up_to_normal_events_manual_ack); + private const string Group = nameof(Group); + private const int BufferCount = 10; + private const int EventWriteCount = BufferCount * 2; - readonly Fixture _fixture; + private readonly Fixture _fixture; public happy_case_catching_up_to_normal_events_manual_ack(Fixture fixture) => _fixture = fixture; [Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + public async Task Test() { + await _fixture.Subscription!.Messages.OfType() + .Take(_fixture.Events.Length) + .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + } public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; + public readonly EventData[] Events; + + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - PersistentSubscription? _subscription; public Fixture() { - _events = CreateTestEvents(EventWriteCount).ToArray(); - _eventsReceived = new(); + Events = CreateTestEvents(EventWriteCount).ToArray(); } - public Task EventsReceived => _eventsReceived.Task; - protected override async Task Given() { - foreach (var e in _events) + foreach (var e in Events) await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); + await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - - if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); + Subscription = Client.SubscribeToStream(Stream, Group, bufferSize: BufferCount, + userCredentials: TestCredentials.Root); } protected override Task When() => Task.CompletedTask; - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs new file mode 100644 index 000000000..43a456b1b --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs @@ -0,0 +1,67 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class happy_case_catching_up_to_normal_events_manual_ack_obsolete : IClassFixture { + const string Stream = nameof(happy_case_catching_up_to_normal_events_manual_ack_obsolete); + const string Group = nameof(Group); + const int BufferCount = 10; + const int EventWriteCount = BufferCount * 2; + + readonly Fixture _fixture; + + public happy_case_catching_up_to_normal_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + + public class Fixture : EventStoreClientFixture { + readonly EventData[] _events; + readonly TaskCompletionSource _eventsReceived; + int _eventReceivedCount; + + PersistentSubscription? _subscription; + + public Fixture() { + _events = CreateTestEvents(EventWriteCount).ToArray(); + _eventsReceived = new(); + } + + public Task EventsReceived => _eventsReceived.Task; + + protected override async Task Given() { + foreach (var e in _events) + await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); + + await Client.CreateToStreamAsync( + Stream, + Group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + + if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) + _eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + _eventsReceived.TrySetException(e); + }, + bufferSize: BufferCount, + userCredentials: TestCredentials.Root + ); + } + + protected override Task When() => Task.CompletedTask; + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs index d055a0903..b4f957d47 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs @@ -2,65 +2,51 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; public class happy_case_writing_and_subscribing_to_normal_events_manual_ack : IClassFixture { - const string Stream = nameof(happy_case_writing_and_subscribing_to_normal_events_manual_ack); - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; + private const string Stream = nameof(happy_case_writing_and_subscribing_to_normal_events_manual_ack); + private const string Group = nameof(Group); + private const int BufferCount = 10; + private const int EventWriteCount = BufferCount * 2; - readonly Fixture _fixture; + private readonly Fixture _fixture; public happy_case_writing_and_subscribing_to_normal_events_manual_ack(Fixture fixture) => _fixture = fixture; [Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + public async Task Test() { + await _fixture.Subscription!.Messages.OfType() + .Take(_fixture.Events.Length) + .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + } public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; + public readonly EventData[] Events; - PersistentSubscription? _subscription; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _events = CreateTestEvents(EventWriteCount).ToArray(); - _eventsReceived = new(); + Events = CreateTestEvents(EventWriteCount).ToArray(); } - public Task EventsReceived => _eventsReceived.Task; - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.End, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); + await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.End, resolveLinkTos: true), + userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); + Subscription = Client.SubscribeToStream(Stream, Group, bufferSize: BufferCount, + userCredentials: TestCredentials.Root); } protected override async Task When() { - foreach (var e in _events) + foreach (var e in Events) await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs new file mode 100644 index 000000000..44377f7cf --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs @@ -0,0 +1,67 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete + : IClassFixture { + const string Stream = nameof(happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete); + const string Group = nameof(Group); + const int BufferCount = 10; + const int EventWriteCount = BufferCount * 2; + + readonly Fixture _fixture; + + public happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + + public class Fixture : EventStoreClientFixture { + readonly EventData[] _events; + readonly TaskCompletionSource _eventsReceived; + int _eventReceivedCount; + + PersistentSubscription? _subscription; + + public Fixture() { + _events = CreateTestEvents(EventWriteCount).ToArray(); + _eventsReceived = new(); + } + + public Task EventsReceived => _eventsReceived.Task; + + protected override async Task Given() { + await Client.CreateToStreamAsync( + Stream, + Group, + new(startFrom: StreamPosition.End, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) + _eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + _eventsReceived.TrySetException(e); + }, + bufferSize: BufferCount, + userCredentials: TestCredentials.Root + ); + } + + protected override async Task When() { + foreach (var e in _events) + await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); + } + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point.cs index 2d459e4fe..09020a8df 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point.cs @@ -2,125 +2,94 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; public class update_existing_with_check_point : IClassFixture { - const string Stream = nameof(update_existing_with_check_point); - const string Group = "existing-with-check-point"; - readonly Fixture _fixture; + private const string Stream = nameof(update_existing_with_check_point); + private const string Group = "existing-with-check-point"; + private readonly Fixture _fixture; - public update_existing_with_check_point(Fixture fixture) => _fixture = fixture; + public update_existing_with_check_point(Fixture fixture) { + _fixture = fixture; + } [Fact] public async Task resumes_from_check_point() { - var resumedEvent = await _fixture.Resumed.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(_fixture.CheckPoint.Next(), resumedEvent.Event.EventNumber); + await using var subscription = + _fixture.Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); + + var resolvedEvent = await subscription.Messages + .OfType() + .Select(e => e.ResolvedEvent) + .FirstAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(_fixture.CheckPoint.Next(), resolvedEvent.Event.EventNumber); } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _appeared; - readonly List _appearedEvents; - readonly TaskCompletionSource _checkPointSource; - - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; - readonly EventData[] _events; - readonly TaskCompletionSource _resumedSource; - PersistentSubscription? _firstSubscription; - PersistentSubscription? _secondSubscription; + private readonly EventData[] _events; public Fixture() { - _droppedSource = new(); - _resumedSource = new(); - _checkPointSource = new(); - _appeared = new(); - _appearedEvents = new(); - _events = CreateTestEvents(5).ToArray(); + _events = CreateTestEvents(5).ToArray(); } - public Task Resumed => _resumedSource.Task; - public StreamPosition CheckPoint { get; private set; } + public StreamPosition CheckPoint { get; private set; } protected override async Task Given() { await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, _events); - await Client.CreateToStreamAsync( - Stream, - Group, - new( - checkPointLowerBound: 5, - checkPointAfter: TimeSpan.FromSeconds(1), - startFrom: StreamPosition.Start - ), - userCredentials: TestCredentials.Root - ); - - var checkPointStream = $"$persistentsubscription-{Stream}::{Group}-checkpoint"; - await StreamsClient.SubscribeToStreamAsync( - checkPointStream, - FromStream.Start, - (_, e, _) => { - _checkPointSource.TrySetResult(e); - return Task.CompletedTask; - }, - subscriptionDropped: (_, _, ex) => { - if (ex != null) - _checkPointSource.TrySetException(ex); - else - _checkPointSource.TrySetResult(default); - }, - userCredentials: TestCredentials.Root - ); - - _firstSubscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (s, e, _, _) => { - _appearedEvents.Add(e); - await s.Ack(e); - - if (_appearedEvents.Count == _events.Length) - _appeared.TrySetResult(true); - }, - (_, reason, ex) => _droppedSource.TrySetResult((reason, ex)), - TestCredentials.Root - ); - - await Task.WhenAll(_appeared.Task, _checkPointSource.Task).WithTimeout(); - - CheckPoint = _checkPointSource.Task.Result.Event.Data.ParseStreamPosition(); + await Client.CreateToStreamAsync(Stream, Group, + new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root); + + await using var subscription = + Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); + + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + + await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); + + return; + + async Task Subscribe() { + var count = 0; + + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + count++; + + await subscription.Ack(resolvedEvent); + if (count >= _events.Length) { + break; + } + } + } + + async Task WaitForCheckpoint() { + await using var subscription = StreamsClient.SubscribeToStream( + $"$persistentsubscription-{Stream}::{Group}-checkpoint", FromStream.Start, + userCredentials: TestCredentials.Root); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event (var resolvedEvent)) { + continue; + } + + CheckPoint = resolvedEvent.Event.Data.ParseStreamPosition(); + return; + } + } } protected override async Task When() { // Force restart of the subscription - await Client.UpdateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - await _droppedSource.Task.WithTimeout(); - - _secondSubscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (s, e, _, _) => { - _resumedSource.TrySetResult(e); - await s.Ack(e); - }, - (_, reason, ex) => { - if (ex is not null) - _resumedSource.TrySetException(ex); - else - _resumedSource.TrySetResult(default); - }, - TestCredentials.Root - ); + await Client.UpdateToStreamAsync(Stream, Group, new(), userCredentials: TestCredentials.Root); await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, CreateTestEvents(1)); } - - public override Task DisposeAsync() { - _firstSubscription?.Dispose(); - _secondSubscription?.Dispose(); - return base.DisposeAsync(); - } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point_obsolete.cs new file mode 100644 index 000000000..37efd167a --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point_obsolete.cs @@ -0,0 +1,124 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class update_existing_with_check_point_obsolete + : IClassFixture { + const string Stream = nameof(update_existing_with_check_point_obsolete); + const string Group = "existing-with-check-point"; + readonly Fixture _fixture; + + public update_existing_with_check_point_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task resumes_from_check_point() { + var resumedEvent = await _fixture.Resumed.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(_fixture.CheckPoint.Next(), resumedEvent.Event.EventNumber); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource _appeared; + readonly List _appearedEvents; + + readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; + readonly EventData[] _events; + readonly TaskCompletionSource _resumedSource; + PersistentSubscription? _firstSubscription; + PersistentSubscription? _secondSubscription; + + public Fixture() { + _droppedSource = new(); + _resumedSource = new(); + _appeared = new(); + _appearedEvents = new(); + _events = CreateTestEvents(5).ToArray(); + } + + public Task Resumed => _resumedSource.Task; + public StreamPosition CheckPoint { get; private set; } + + protected override async Task Given() { + await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, _events); + + await Client.CreateToStreamAsync( + Stream, + Group, + new( + checkPointLowerBound: 5, + checkPointAfter: TimeSpan.FromSeconds(1), + startFrom: StreamPosition.Start + ), + userCredentials: TestCredentials.Root + ); + + var checkPointStream = $"$persistentsubscription-{Stream}::{Group}-checkpoint"; + + _firstSubscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (s, e, _, _) => { + _appearedEvents.Add(e); + await s.Ack(e); + + if (_appearedEvents.Count == _events.Length) + _appeared.TrySetResult(true); + }, + (_, reason, ex) => _droppedSource.TrySetResult((reason, ex)), + TestCredentials.Root + ); + + await Task.WhenAll(_appeared.Task.WithTimeout(), Checkpointed()); + + return; + + async Task Checkpointed() { + await using var subscription = StreamsClient.SubscribeToStream(checkPointStream, FromStream.Start, + userCredentials: TestCredentials.Root); + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + CheckPoint = resolvedEvent.Event.Data.ParseStreamPosition(); + return; + } + + throw new InvalidOperationException(); + } + } + + protected override async Task When() { + // Force restart of the subscription + await Client.UpdateToStreamAsync( + Stream, + Group, + new(), + userCredentials: TestCredentials.Root + ); + + await _droppedSource.Task.WithTimeout(); + + _secondSubscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (s, e, _, _) => { + _resumedSource.TrySetResult(e); + await s.Ack(e); + }, + (_, reason, ex) => { + if (ex is not null) + _resumedSource.TrySetException(ex); + else + _resumedSource.TrySetResult(default); + }, + TestCredentials.Root + ); + + await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, CreateTestEvents(1)); + } + + public override Task DisposeAsync() { + _firstSubscription?.Dispose(); + _secondSubscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs index 72b59b778..e472aec9d 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs @@ -1,62 +1,50 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; -public class update_existing_with_subscribers - : IClassFixture { - const string Stream = nameof(update_existing_with_subscribers); - const string Group = "existing"; - readonly Fixture _fixture; +public class update_existing_with_subscribers : IClassFixture { + private const string Stream = nameof(update_existing_with_subscribers); + private const string Group = "existing"; + private readonly Fixture _fixture; public update_existing_with_subscribers(Fixture fixture) => _fixture = fixture; [Fact] public async Task existing_subscriptions_are_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); + var ex = await Assert.ThrowsAsync(async () => { + while (await _fixture.Enumerator!.MoveNextAsync()) { + } + }).WithTimeout(); -#if NET Assert.Equal(Stream, ex.StreamName); Assert.Equal(Group, ex.GroupName); -#endif } public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; - PersistentSubscription? _subscription; - - public Fixture() => _droppedSource = new(); - - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _droppedSource.Task; + private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; + public IAsyncEnumerator? Enumerator { get; private set; } protected override async Task Given() { await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, CreateTestEvents()); - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - delegate { return Task.CompletedTask; }, - (_, reason, ex) => _droppedSource.TrySetResult((reason, ex)), - TestCredentials.Root - ); + await Client.CreateToStreamAsync(Stream, Group, new(), userCredentials: TestCredentials.Root); + + _subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); + Enumerator = _subscription.Messages.GetAsyncEnumerator(); + + await Enumerator.MoveNextAsync(); } protected override Task When() => - Client.UpdateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + Client.UpdateToStreamAsync(Stream, Group, new(), userCredentials: TestCredentials.Root); + + public override async Task DisposeAsync() { + if (Enumerator is not null) { + await Enumerator.DisposeAsync(); + } + + if (_subscription is not null) { + await _subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers_obsolete.cs new file mode 100644 index 000000000..fef96712c --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers_obsolete.cs @@ -0,0 +1,61 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class update_existing_with_subscribers_obsolete + : IClassFixture { + const string Stream = nameof(update_existing_with_subscribers_obsolete); + const string Group = "existing"; + readonly Fixture _fixture; + + public update_existing_with_subscribers_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task existing_subscriptions_are_dropped() { + var (reason, exception) = await _fixture.Dropped.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + + Assert.Equal(Stream, ex.StreamName); + Assert.Equal(Group, ex.GroupName); + } + + public class Fixture : EventStoreClientFixture { + readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; + PersistentSubscription? _subscription; + + public Fixture() => _droppedSource = new(); + + public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _droppedSource.Task; + + protected override async Task Given() { + await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, CreateTestEvents()); + await Client.CreateToStreamAsync( + Stream, + Group, + new(), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + delegate { return Task.CompletedTask; }, + (_, reason, ex) => _droppedSource.TrySetResult((reason, ex)), + TestCredentials.Root + ); + } + + protected override Task When() => + Client.UpdateToStreamAsync( + Stream, + Group, + new(), + userCredentials: TestCredentials.Root + ); + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack.cs index 68d807a63..267baa684 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack.cs @@ -1,69 +1,54 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; +[Obsolete] public class when_writing_and_subscribing_to_normal_events_manual_nack : IClassFixture { - const string Stream = nameof(when_writing_and_subscribing_to_normal_events_manual_nack); - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; + private const string Stream = nameof(when_writing_and_subscribing_to_normal_events_manual_nack); + private const string Group = nameof(Group); + private const int BufferCount = 10; + private const int EventWriteCount = BufferCount * 2; - readonly Fixture _fixture; + private readonly Fixture _fixture; public when_writing_and_subscribing_to_normal_events_manual_nack(Fixture fixture) => _fixture = fixture; [Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + public async Task Test() { + await _fixture.Subscription!.Messages.OfType() + .Take(1) + .ForEachAwaitAsync(async message => + await _fixture.Subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", + message.ResolvedEvent)) + .WithTimeout(); + } public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; + private readonly EventData[] _events; + public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } public Fixture() { - _events = CreateTestEvents(EventWriteCount) - .ToArray(); - - _eventsReceived = new(); + _events = CreateTestEvents(EventWriteCount).ToArray(); } - public Task EventsReceived => _eventsReceived.Task; - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e); + await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root); - if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); + Subscription = Client.SubscribeToStream(Stream, Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); } protected override async Task When() { - foreach (var e in _events) + foreach (var e in _events) { await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); + } } - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + if (Subscription is not null) { + await Subscription.DisposeAsync(); + } + + await base.DisposeAsync(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs new file mode 100644 index 000000000..f179ad854 --- /dev/null +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs @@ -0,0 +1,70 @@ +namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; + +[Obsolete] +public class when_writing_and_subscribing_to_normal_events_manual_nack_obsolete + : IClassFixture { + const string Stream = nameof(when_writing_and_subscribing_to_normal_events_manual_nack_obsolete); + const string Group = nameof(Group); + const int BufferCount = 10; + const int EventWriteCount = BufferCount * 2; + + readonly Fixture _fixture; + + public when_writing_and_subscribing_to_normal_events_manual_nack_obsolete(Fixture fixture) => _fixture = fixture; + + [Fact] + public async Task Test() => await _fixture.EventsReceived.WithTimeout(); + + public class Fixture : EventStoreClientFixture { + readonly EventData[] _events; + readonly TaskCompletionSource _eventsReceived; + int _eventReceivedCount; + + PersistentSubscription? _subscription; + + public Fixture() { + _events = CreateTestEvents(EventWriteCount) + .ToArray(); + + _eventsReceived = new(); + } + + public Task EventsReceived => _eventsReceived.Task; + + protected override async Task Given() { + await Client.CreateToStreamAsync( + Stream, + Group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + _subscription = await Client.SubscribeToStreamAsync( + Stream, + Group, + async (subscription, e, retryCount, ct) => { + await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e); + + if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) + _eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + _eventsReceived.TrySetException(e); + }, + bufferSize: BufferCount, + userCredentials: TestCredentials.Root + ); + } + + protected override async Task When() { + foreach (var e in _events) + await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); + } + + public override Task DisposeAsync() { + _subscription?.Dispose(); + return base.DisposeAsync(); + } + } +} diff --git a/test/EventStore.Client.Streams.Tests/Bugs/Issue_104.cs b/test/EventStore.Client.Streams.Tests/Bugs/Issue_104.cs index 67815004d..cefd65164 100644 --- a/test/EventStore.Client.Streams.Tests/Bugs/Issue_104.cs +++ b/test/EventStore.Client.Streams.Tests/Bugs/Issue_104.cs @@ -1,6 +1,7 @@ namespace EventStore.Client.Streams.Tests.Bugs; [Trait("Category", "Bug")] +[Obsolete] public class Issue_104(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { [Fact] public async Task subscription_does_not_send_checkpoint_reached_after_disposal() { @@ -49,4 +50,4 @@ await Fixture.Streams.AppendToStreamAsync( var result = await Task.WhenAny(delay, checkpointReachAfterDisposed.Task); result.ShouldBe(delay); // iow 300ms have passed without seeing checkpointReachAfterDisposed } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Bugs/Issue_2544.cs b/test/EventStore.Client.Streams.Tests/Bugs/Issue_2544.cs index 26f9dbd83..10e8b6e75 100644 --- a/test/EventStore.Client.Streams.Tests/Bugs/Issue_2544.cs +++ b/test/EventStore.Client.Streams.Tests/Bugs/Issue_2544.cs @@ -3,6 +3,7 @@ namespace EventStore.Client.Streams.Tests.Bugs; [Trait("Category", "Bug")] +[Obsolete] public class Issue_2544 : IClassFixture { public Issue_2544(ITestOutputHelper output, EventStoreFixture fixture) { Fixture = fixture.With(x => x.CaptureTestRun(output)); @@ -168,4 +169,4 @@ Task EventAppeared(ResolvedEvent e, string streamName) { return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj b/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj index caf586457..e0cf4e59d 100644 --- a/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj +++ b/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj @@ -1,9 +1,9 @@  + + CS0612;xUnit1031 + - - - diff --git a/test/EventStore.Client.Streams.Tests/Security/SecurityFixture.cs b/test/EventStore.Client.Streams.Tests/Security/SecurityFixture.cs index c472581bf..f97789b91 100644 --- a/test/EventStore.Client.Streams.Tests/Security/SecurityFixture.cs +++ b/test/EventStore.Client.Streams.Tests/Security/SecurityFixture.cs @@ -242,7 +242,8 @@ public Task WriteMeta(string streamId, UserCredentials? userCreden ) .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - public async Task SubscribeToStream(string streamId, UserCredentials? userCredentials = default) { + [Obsolete] + public async Task SubscribeToStreamObsolete(string streamId, UserCredentials? userCredentials = default) { var source = new TaskCompletionSource(); using (await Streams.SubscribeToStreamAsync( streamId, @@ -263,7 +264,16 @@ public async Task SubscribeToStream(string streamId, UserCredentials? userCreden } } - public async Task SubscribeToAll(UserCredentials? userCredentials = default) { + public async Task SubscribeToStream(string streamId, UserCredentials? userCredentials = default) { + await using var subscription = + Streams.SubscribeToStream(streamId, FromStream.Start, userCredentials: userCredentials); + await subscription + .Messages.OfType().AnyAsync().AsTask() + .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); + } + + [Obsolete] + public async Task SubscribeToAllObsolete(UserCredentials? userCredentials = default) { var source = new TaskCompletionSource(); using (await Streams.SubscribeToAllAsync( FromAll.Start, @@ -299,4 +309,4 @@ await Streams.SetStreamMetadataAsync( public Task DeleteStream(string streamId, UserCredentials? userCredentials = default) => Streams.TombstoneAsync(streamId, StreamState.Any, userCredentials: userCredentials) .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/all_stream_with_no_acl_security.cs b/test/EventStore.Client.Streams.Tests/Security/all_stream_with_no_acl_security.cs index 215e0f05d..d5ab0cc1a 100644 --- a/test/EventStore.Client.Streams.Tests/Security/all_stream_with_no_acl_security.cs +++ b/test/EventStore.Client.Streams.Tests/Security/all_stream_with_no_acl_security.cs @@ -21,7 +21,7 @@ public async Task reading_and_subscribing_is_not_allowed_when_no_credentials_are await Assert.ThrowsAsync(() => Fixture.ReadAllForward()); await Assert.ThrowsAsync(() => Fixture.ReadAllBackward()); await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture.AllStream)); - await Assert.ThrowsAsync(() => Fixture.SubscribeToAll()); + await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete()); } [Fact] @@ -29,7 +29,7 @@ public async Task reading_and_subscribing_is_not_allowed_for_usual_user() { await Assert.ThrowsAsync(() => Fixture.ReadAllForward(TestCredentials.TestUser1)); await Assert.ThrowsAsync(() => Fixture.ReadAllBackward(TestCredentials.TestUser1)); await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture.AllStream, TestCredentials.TestUser1)); - await Assert.ThrowsAsync(() => Fixture.SubscribeToAll(TestCredentials.TestUser1)); + await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete(TestCredentials.TestUser1)); } [Fact] @@ -37,7 +37,7 @@ public async Task reading_and_subscribing_is_allowed_for_admin_user() { await Fixture.ReadAllForward(TestCredentials.TestAdmin); await Fixture.ReadAllBackward(TestCredentials.TestAdmin); await Fixture.ReadMeta(SecurityFixture.AllStream, TestCredentials.TestAdmin); - await Fixture.SubscribeToAll(TestCredentials.TestAdmin); + await Fixture.SubscribeToAllObsolete(TestCredentials.TestAdmin); } [Fact] diff --git a/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security.cs b/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security.cs index 0d41c4450..6dd93a3c9 100644 --- a/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security.cs +++ b/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security.cs @@ -15,6 +15,7 @@ public async Task operations_on_system_stream_succeed_for_authorized_user() { await Fixture.WriteMeta(stream, TestCredentials.TestUser1); await Fixture.SubscribeToStream(stream, TestCredentials.TestUser1); + await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser1); await Fixture.DeleteStream(stream, TestCredentials.TestUser1); } @@ -35,6 +36,7 @@ await Assert.ThrowsAsync( await Assert.ThrowsAsync(() => Fixture.WriteMeta(stream, TestCredentials.TestUser2)); await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(stream, TestCredentials.TestUser2)); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser2)); await Assert.ThrowsAsync(() => Fixture.DeleteStream(stream, TestCredentials.TestUser2)); } @@ -52,6 +54,7 @@ public async Task operations_on_system_stream_fail_for_anonymous_user() { await Assert.ThrowsAsync(() => Fixture.WriteMeta(stream)); await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(stream)); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(stream)); await Assert.ThrowsAsync(() => Fixture.DeleteStream(stream)); } @@ -69,6 +72,7 @@ public async Task operations_on_system_stream_succeed_for_admin() { await Fixture.WriteMeta(stream, TestCredentials.TestAdmin); await Fixture.SubscribeToStream(stream, TestCredentials.TestAdmin); + await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestAdmin); await Fixture.DeleteStream(stream, TestCredentials.TestAdmin); } @@ -83,4 +87,4 @@ protected override Task When() { return Streams.SetSystemSettingsAsync(settings, userCredentials: TestCredentials.TestAdmin); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security_for_all.cs b/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security_for_all.cs index 8324451ef..df1153449 100644 --- a/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security_for_all.cs +++ b/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security_for_all.cs @@ -14,6 +14,7 @@ public async Task operations_on_system_stream_succeeds_for_user() { await Fixture.WriteMeta(stream, TestCredentials.TestUser1); await Fixture.SubscribeToStream(stream, TestCredentials.TestUser1); + await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser1); await Fixture.DeleteStream(stream, TestCredentials.TestUser1); } @@ -30,6 +31,7 @@ public async Task operations_on_system_stream_fail_for_anonymous_user() { await Fixture.WriteMeta(stream); await Fixture.SubscribeToStream(stream); + await Fixture.SubscribeToStreamObsolete(stream); await Fixture.DeleteStream(stream); } @@ -47,6 +49,7 @@ public async Task operations_on_system_stream_succeed_for_admin() { await Fixture.WriteMeta(stream, TestCredentials.TestAdmin); await Fixture.SubscribeToStream(stream, TestCredentials.TestAdmin); + await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestAdmin); await Fixture.DeleteStream(stream, TestCredentials.TestAdmin); } @@ -66,4 +69,4 @@ protected override Task When() { return Streams.SetSystemSettingsAsync(settings, userCredentials: TestCredentials.TestAdmin); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/overriden_user_stream_security.cs b/test/EventStore.Client.Streams.Tests/Security/overriden_user_stream_security.cs index ce1ebe1e9..a7cf4a5e4 100644 --- a/test/EventStore.Client.Streams.Tests/Security/overriden_user_stream_security.cs +++ b/test/EventStore.Client.Streams.Tests/Security/overriden_user_stream_security.cs @@ -15,6 +15,7 @@ public async Task operations_on_user_stream_succeeds_for_authorized_user() { await Fixture.WriteMeta(stream, TestCredentials.TestUser1); await Fixture.SubscribeToStream(stream, TestCredentials.TestUser1); + await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser1); await Fixture.DeleteStream(stream, TestCredentials.TestUser1); } @@ -31,6 +32,7 @@ public async Task operations_on_user_stream_fail_for_not_authorized_user() { await Assert.ThrowsAsync(() => Fixture.WriteMeta(stream, TestCredentials.TestUser2)); await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(stream, TestCredentials.TestUser2)); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser2)); await Assert.ThrowsAsync(() => Fixture.DeleteStream(stream, TestCredentials.TestUser2)); } @@ -47,6 +49,7 @@ public async Task operations_on_user_stream_fail_for_anonymous_user() { await Assert.ThrowsAsync(() => Fixture.WriteMeta(stream)); await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(stream)); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(stream)); await Assert.ThrowsAsync(() => Fixture.DeleteStream(stream)); } @@ -64,6 +67,7 @@ public async Task operations_on_user_stream_succeed_for_admin() { await Fixture.WriteMeta(stream, TestCredentials.TestAdmin); await Fixture.SubscribeToStream(stream, TestCredentials.TestAdmin); + await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestAdmin); await Fixture.DeleteStream(stream, TestCredentials.TestAdmin); } @@ -74,4 +78,4 @@ protected override Task When() { return Streams.SetSystemSettingsAsync(settings, userCredentials: TestCredentials.TestAdmin); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/subscribe_to_all_security.cs b/test/EventStore.Client.Streams.Tests/Security/subscribe_to_all_security.cs index 48b62a52d..c9964c862 100644 --- a/test/EventStore.Client.Streams.Tests/Security/subscribe_to_all_security.cs +++ b/test/EventStore.Client.Streams.Tests/Security/subscribe_to_all_security.cs @@ -4,18 +4,18 @@ namespace EventStore.Client.Streams.Tests; public class subscribe_to_all_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { [Fact] public async Task subscribing_to_all_with_not_existing_credentials_is_not_authenticated() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToAll(TestCredentials.TestBadUser)); + await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete(TestCredentials.TestBadUser)); [Fact] - public async Task subscribing_to_all_with_no_credentials_is_denied() => await Assert.ThrowsAsync(() => Fixture.SubscribeToAll()); + public async Task subscribing_to_all_with_no_credentials_is_denied() => await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete()); [Fact] public async Task subscribing_to_all_with_not_authorized_user_credentials_is_denied() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToAll(TestCredentials.TestUser2)); + await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete(TestCredentials.TestUser2)); [Fact] - public async Task subscribing_to_all_with_authorized_user_credentials_succeeds() => await Fixture.SubscribeToAll(TestCredentials.TestUser1); + public async Task subscribing_to_all_with_authorized_user_credentials_succeeds() => await Fixture.SubscribeToAllObsolete(TestCredentials.TestUser1); [Fact] - public async Task subscribing_to_all_with_admin_user_credentials_succeeds() => await Fixture.SubscribeToAll(TestCredentials.TestAdmin); + public async Task subscribing_to_all_with_admin_user_credentials_succeeds() => await Fixture.SubscribeToAllObsolete(TestCredentials.TestAdmin); } \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security.cs b/test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security.cs index 1bcb43ae1..64e459733 100644 --- a/test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security.cs +++ b/test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security.cs @@ -1,10 +1,14 @@ -namespace EventStore.Client.Streams.Tests; +namespace EventStore.Client.Streams.Tests; [Trait("Category", "Security")] -public class subscribe_to_stream_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +[Obsolete] +public class subscribe_to_stream_security(ITestOutputHelper output, SecurityFixture fixture) + : EventStoreTests(output, fixture) { [Fact] public async Task subscribing_to_stream_with_not_existing_credentials_is_not_authenticated() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.ReadStream, TestCredentials.TestBadUser)); + await Assert.ThrowsAsync( + () => Fixture.SubscribeToStream(SecurityFixture.ReadStream, TestCredentials.TestBadUser) + ); [Fact] public async Task subscribing_to_stream_with_no_credentials_is_denied() => @@ -12,7 +16,9 @@ public async Task subscribing_to_stream_with_no_credentials_is_denied() => [Fact] public async Task subscribing_to_stream_with_not_authorized_user_credentials_is_denied() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.ReadStream, TestCredentials.TestUser2)); + await Assert.ThrowsAsync( + () => Fixture.SubscribeToStream(SecurityFixture.ReadStream, TestCredentials.TestUser2) + ); [Fact] public async Task reading_stream_with_authorized_user_credentials_succeeds() { @@ -34,7 +40,9 @@ public async Task subscribing_to_no_acl_stream_succeeds_when_no_credentials_are_ [Fact] public async Task subscribing_to_no_acl_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.NoAclStream, TestCredentials.TestBadUser)); + await Assert.ThrowsAsync( + () => Fixture.SubscribeToStream(SecurityFixture.NoAclStream, TestCredentials.TestBadUser) + ); [Fact] public async Task subscribing_to_no_acl_stream_succeeds_when_any_existing_user_credentials_are_passed() { @@ -56,8 +64,11 @@ public async Task subscribing_to_all_access_normal_stream_succeeds_when_no_crede } [Fact] - public async Task subscribing_to_all_access_normal_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser)); + public async Task + subscribing_to_all_access_normal_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => + await Assert.ThrowsAsync( + () => Fixture.SubscribeToStream(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser) + ); [Fact] public async Task subscribing_to_all_access_normal_stream_succeeds_when_any_existing_user_credentials_are_passed() { @@ -71,4 +82,4 @@ public async Task subscribing_to_all_access_normal_streamm_succeeds_when_admin_u await Fixture.AppendStream(SecurityFixture.NormalAllStream, TestCredentials.TestAdmin); await Fixture.SubscribeToStream(SecurityFixture.NormalAllStream, TestCredentials.TestAdmin); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security_obsolete.cs new file mode 100644 index 000000000..f5018d1c5 --- /dev/null +++ b/test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security_obsolete.cs @@ -0,0 +1,75 @@ +namespace EventStore.Client.Streams.Tests; + +[Trait("Category", "Security")] +[Obsolete] +public class subscribe_to_stream_security_obsolete(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { + [Fact] + public async Task subscribing_to_stream_with_not_existing_credentials_is_not_authenticated() => + await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture.ReadStream, TestCredentials.TestBadUser)); + + [Fact] + public async Task subscribing_to_stream_with_no_credentials_is_denied() => + await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture.ReadStream)); + + [Fact] + public async Task subscribing_to_stream_with_not_authorized_user_credentials_is_denied() => + await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture.ReadStream, TestCredentials.TestUser2)); + + [Fact] + public async Task reading_stream_with_authorized_user_credentials_succeeds() { + await Fixture.AppendStream(SecurityFixture.ReadStream, TestCredentials.TestUser1); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.ReadStream, TestCredentials.TestUser1); + } + + [Fact] + public async Task reading_stream_with_admin_user_credentials_succeeds() { + await Fixture.AppendStream(SecurityFixture.ReadStream, TestCredentials.TestAdmin); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.ReadStream, TestCredentials.TestAdmin); + } + + [AnonymousAccess.Fact] + public async Task subscribing_to_no_acl_stream_succeeds_when_no_credentials_are_passed() { + await Fixture.AppendStream(SecurityFixture.NoAclStream); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.NoAclStream); + } + + [Fact] + public async Task subscribing_to_no_acl_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => + await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture.NoAclStream, TestCredentials.TestBadUser)); + + [Fact] + public async Task subscribing_to_no_acl_stream_succeeds_when_any_existing_user_credentials_are_passed() { + await Fixture.AppendStream(SecurityFixture.NoAclStream, TestCredentials.TestUser1); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.NoAclStream, TestCredentials.TestUser1); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.NoAclStream, TestCredentials.TestUser2); + } + + [Fact] + public async Task subscribing_to_no_acl_stream_succeeds_when_admin_user_credentials_are_passed() { + await Fixture.AppendStream(SecurityFixture.NoAclStream, TestCredentials.TestAdmin); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.NoAclStream, TestCredentials.TestAdmin); + } + + [AnonymousAccess.Fact] + public async Task subscribing_to_all_access_normal_stream_succeeds_when_no_credentials_are_passed() { + await Fixture.AppendStream(SecurityFixture.NormalAllStream); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.NormalAllStream); + } + + [Fact] + public async Task subscribing_to_all_access_normal_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => + await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser)); + + [Fact] + public async Task subscribing_to_all_access_normal_stream_succeeds_when_any_existing_user_credentials_are_passed() { + await Fixture.AppendStream(SecurityFixture.NormalAllStream, TestCredentials.TestUser1); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.NormalAllStream, TestCredentials.TestUser1); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.NormalAllStream, TestCredentials.TestUser2); + } + + [Fact] + public async Task subscribing_to_all_access_normal_streamm_succeeds_when_admin_user_credentials_are_passed() { + await Fixture.AppendStream(SecurityFixture.NormalAllStream, TestCredentials.TestAdmin); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.NormalAllStream, TestCredentials.TestAdmin); + } +} diff --git a/test/EventStore.Client.Streams.Tests/Security/system_stream_security.cs b/test/EventStore.Client.Streams.Tests/Security/system_stream_security.cs index bbf482cbe..66558ab91 100644 --- a/test/EventStore.Client.Streams.Tests/Security/system_stream_security.cs +++ b/test/EventStore.Client.Streams.Tests/Security/system_stream_security.cs @@ -15,6 +15,7 @@ public async Task operations_on_system_stream_with_no_acl_set_fail_for_non_admin await Assert.ThrowsAsync(() => Fixture.WriteMeta("$system-no-acl", TestCredentials.TestUser1)); await Assert.ThrowsAsync(() => Fixture.SubscribeToStream("$system-no-acl", TestCredentials.TestUser1)); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete("$system-no-acl", TestCredentials.TestUser1)); } [Fact] @@ -29,6 +30,7 @@ public async Task operations_on_system_stream_with_no_acl_set_succeed_for_admin( await Fixture.WriteMeta("$system-no-acl", TestCredentials.TestAdmin); await Fixture.SubscribeToStream("$system-no-acl", TestCredentials.TestAdmin); + await Fixture.SubscribeToStreamObsolete("$system-no-acl", TestCredentials.TestAdmin); } [Fact] @@ -43,6 +45,7 @@ public async Task operations_on_system_stream_with_acl_set_to_usual_user_fail_fo await Assert.ThrowsAsync(() => Fixture.WriteMeta(SecurityFixture.SystemAclStream, TestCredentials.TestUser2, TestCredentials.TestUser1.Username)); await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.SystemAclStream, TestCredentials.TestUser2)); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture.SystemAclStream, TestCredentials.TestUser2)); } [Fact] @@ -56,6 +59,7 @@ public async Task operations_on_system_stream_with_acl_set_to_usual_user_succeed await Fixture.WriteMeta(SecurityFixture.SystemAclStream, TestCredentials.TestUser1, TestCredentials.TestUser1.Username); await Fixture.SubscribeToStream(SecurityFixture.SystemAclStream, TestCredentials.TestUser1); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.SystemAclStream, TestCredentials.TestUser1); } [Fact] @@ -69,6 +73,7 @@ public async Task operations_on_system_stream_with_acl_set_to_usual_user_succeed await Fixture.WriteMeta(SecurityFixture.SystemAclStream, TestCredentials.TestAdmin, TestCredentials.TestUser1.Username); await Fixture.SubscribeToStream(SecurityFixture.SystemAclStream, TestCredentials.TestAdmin); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.SystemAclStream, TestCredentials.TestAdmin); } [Fact] @@ -89,6 +94,7 @@ await Assert.ThrowsAsync( ); await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.SystemAdminStream, TestCredentials.TestUser1)); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture.SystemAdminStream, TestCredentials.TestUser1)); } [Fact] @@ -102,6 +108,7 @@ public async Task operations_on_system_stream_with_acl_set_to_admins_succeed_for await Fixture.WriteMeta(SecurityFixture.SystemAdminStream, TestCredentials.TestAdmin, SystemRoles.Admins); await Fixture.SubscribeToStream(SecurityFixture.SystemAdminStream, TestCredentials.TestAdmin); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.SystemAdminStream, TestCredentials.TestAdmin); } [AnonymousAccess.Fact] @@ -115,6 +122,7 @@ public async Task operations_on_system_stream_with_acl_set_to_all_succeed_for_no await Fixture.WriteMeta(SecurityFixture.SystemAllStream, role: SystemRoles.All); await Fixture.SubscribeToStream(SecurityFixture.SystemAllStream); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.SystemAllStream); } [Fact] @@ -128,6 +136,7 @@ public async Task operations_on_system_stream_with_acl_set_to_all_succeed_for_us await Fixture.WriteMeta(SecurityFixture.SystemAllStream, TestCredentials.TestUser1, SystemRoles.All); await Fixture.SubscribeToStream(SecurityFixture.SystemAllStream, TestCredentials.TestUser1); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.SystemAllStream, TestCredentials.TestUser1); } [Fact] @@ -141,5 +150,6 @@ public async Task operations_on_system_stream_with_acl_set_to_all_succeed_for_ad await Fixture.WriteMeta(SecurityFixture.SystemAllStream, TestCredentials.TestAdmin, SystemRoles.All); await Fixture.SubscribeToStream(SecurityFixture.SystemAllStream, TestCredentials.TestAdmin); + await Fixture.SubscribeToStreamObsolete(SecurityFixture.SystemAllStream, TestCredentials.TestAdmin); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs b/test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs index db9601d4f..2dc9912e5 100644 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs +++ b/test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs @@ -4,6 +4,7 @@ namespace EventStore.Client.Streams.Tests.Subscriptions; [Trait("Category", "Subscriptions")] +[Obsolete] public class @reconnection(ITestOutputHelper output, ReconnectionFixture fixture) : EventStoreTests(output, fixture) { [Theory] [InlineData(4, 5000, 0, 30000)] @@ -138,6 +139,7 @@ Func OnReceive() { } } + [Obsolete] async Task SubscribeToStream( string stream, StreamPosition? checkpoint, @@ -173,4 +175,4 @@ CancellationToken cancellationToken if (resubscribe) _ = SubscribeToStream(stream, checkpoint, onReceive, onDrop, cancellationToken); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all.cs b/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all.cs index 9ea4add2b..f0f7cb9dc 100644 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all.cs +++ b/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all.cs @@ -2,590 +2,430 @@ namespace EventStore.Client.Streams.Tests.Subscriptions; [Trait("Category", "Subscriptions")] [Trait("Category", "Target:All")] -public class subscribe_to_all(ITestOutputHelper output, SubscriptionsFixture fixture) : EventStoreTests(output, fixture) { +public class subscribe_to_all(ITestOutputHelper output, SubscriptionsFixture fixture) + : EventStoreTests(output, fixture) { [Fact] public async Task receives_all_events_from_start() { - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - var pageSize = seedEvents.Length / 2; - + var pageSize = seedEvents.Length / 2; + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - + foreach (var evt in seedEvents.Take(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, + new[] { evt }); - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(FromAll.Start, OnReceived, false, OnDropped) - .WithTimeout(); + await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.Start); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + Assert.True(await enumerator.MoveNextAsync()); - await receivedAllEvents.Task.WithTimeout(); + Assert.IsType(enumerator.Current); - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, + new[] { evt }); + + await Subscribe().WithTimeout(); - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - return; - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + availableEvents.Remove(resolvedEvent.Event.EventId); + + if (availableEvents.Count == 0) { + return; + } } - - return Task.CompletedTask; } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); } - + [Fact] public async Task receives_all_events_from_end() { - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(FromAll.End, OnReceived, false, OnDropped) - .WithTimeout(); + + await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.End); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); // add the events we want to receive after we start the subscription foreach (var evt in seedEvents) - await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - await receivedAllEvents.Task.WithTimeout(); + await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, + new[] { evt }); - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + await Subscribe().WithTimeout(); - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - return; - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + availableEvents.Remove(resolvedEvent.OriginalEvent.EventId); + + if (availableEvents.Count == 0) { + return; + } } - - return Task.CompletedTask; } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); } - + [Fact] public async Task receives_all_events_from_position() { - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - var pageSize = seedEvents.Length / 2; - + var pageSize = seedEvents.Length / 2; + // only the second half of the events will be received var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); - + IWriteResult writeResult = new SuccessResult(); foreach (var evt in seedEvents.Take(pageSize)) - writeResult = await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + writeResult = await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", + StreamState.NoStream, new[] { evt }); var position = FromAll.After(writeResult.LogPosition); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(position, OnReceived, false, OnDropped) - .WithTimeout(); - foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + await using var subscription = Fixture.Streams.SubscribeToAll(position); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); - await receivedAllEvents.Task.WithTimeout(); + Assert.IsType(enumerator.Current); + + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, + new[] { evt }); - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + await Subscribe().WithTimeout(); - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - return; - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + availableEvents.Remove(resolvedEvent.Event.EventId); + + if (availableEvents.Count == 0) { + return; + } } - - return Task.CompletedTask; } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); } - + [Fact] public async Task receives_all_events_with_resolved_links() { var streamName = Fixture.GetStreamName(); - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(3).ToArray(); + var seedEvents = Fixture.CreateTestEvents(3).ToArray(); var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(FromAll.Start, OnReceived, true, OnDropped) - .WithTimeout(); - - await receivedAllEvents.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - + + await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.Start, true); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); + + await Subscribe().WithTimeout(); + return; - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}"); - if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { - Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); - return Task.CompletedTask; - } - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + availableEvents.Remove(resolvedEvent.Event.EventId); + + if (availableEvents.Count == 0) { + return; + } } - - return Task.CompletedTask; } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); } - + [Theory] - [MemberData(nameof(SubscriptionFilter.TestCases), MemberType= typeof(SubscriptionFilter))] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] public async Task receives_all_filtered_events_from_start(SubscriptionFilter filter) { var streamPrefix = $"{nameof(receives_all_filtered_events_from_start)}-{filter.Name}-{Guid.NewGuid():N}"; - + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); - - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - var checkpointReached = new TaskCompletionSource(); - + var seedEvents = Fixture.CreateTestEvents(64) .Select(evt => filter.PrepareEvent(streamPrefix, evt)) .ToArray(); var pageSize = seedEvents.Length / 2; - + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); // add noise - await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); - - var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); + await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, + Fixture.CreateTestEvents(3)); + + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start) + .Messages.CountAsync(); Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); // Debugging: // await foreach (var evt in Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start)) // Fixture.Log.Debug("Read event {EventId} from {StreamId}.", evt.OriginalEvent.EventId, evt.OriginalEvent.EventStreamId); - + // add some of the events we want to see before we start the subscription foreach (var evt in seedEvents.Take(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, + new[] { evt }); - var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(FromAll.Start, OnReceived, false, OnDropped, filterOptions) - .WithTimeout(); + var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1); + + await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); // add some of the events we want to see after we start the subscription foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - // wait until all events were received and at least one checkpoint was reached? - await receivedAllEvents.Task.WithTimeout(); - await checkpointReached.Task.WithTimeout(); - - // await Task.WhenAll(receivedAllEvents.Task, checkpointReached.Task).WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - if (availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId) == 0) { - Fixture.Log.Error( - "Received unexpected event {EventId} from {StreamId}", - re.OriginalEvent.EventId, - re.OriginalEvent.EventStreamId - ); - - receivedAllEvents.TrySetException( - new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") - ); - } - else { - Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}.", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); - } + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, + new[] { evt }); - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events.", seedEvents.Length); - } + bool checkpointReached = false; - return Task.CompletedTask; - } + await Subscribe().WithTimeout(); - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) { - subscriptionDropped.SetResult(new(reason, ex)); - if (reason != SubscriptionDroppedReason.Disposed) { - receivedAllEvents.TrySetException(ex!); - checkpointReached.TrySetException(ex!); - } - } + Assert.True(checkpointReached); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + switch (enumerator.Current) { + case StreamMessage.AllStreamCheckpointReached: + checkpointReached = true; - Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { - Fixture.Log.Verbose( - "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", - position, seedEvents.Length - availableEvents.Count, seedEvents.Length - ); - checkpointReached.TrySetResult(true); - return Task.CompletedTask; + break; + case StreamMessage.Event(var resolvedEvent): { + availableEvents.Remove(resolvedEvent.Event.EventId); + + if (availableEvents.Count == 0) { + return; + } + + break; + } + } + } } } - + [Theory] - [MemberData(nameof(SubscriptionFilter.TestCases), MemberType= typeof(SubscriptionFilter))] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] public async Task receives_all_filtered_events_from_end(SubscriptionFilter filter) { var streamPrefix = $"{nameof(receives_all_filtered_events_from_end)}-{filter.Name}-{Guid.NewGuid():N}"; - + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); - - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - var checkpointReached = new TaskCompletionSource(); - + var seedEvents = Fixture.CreateTestEvents(64) .Select(evt => filter.PrepareEvent(streamPrefix, evt)) .ToArray(); - + var pageSize = seedEvents.Length / 2; - + // only the second half of the events will be received var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); // add noise - await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); - - var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); + await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, + Fixture.CreateTestEvents(3)); + + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start) + .Messages.CountAsync(); Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); - - // add some of the events that are a match to the filter but will not be received + + // Debugging: + // await foreach (var evt in Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start)) + // Fixture.Log.Debug("Read event {EventId} from {StreamId}.", evt.OriginalEvent.EventId, evt.OriginalEvent.EventStreamId); + + // add some of the events we want to see before we start the subscription foreach (var evt in seedEvents.Take(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(FromAll.End, OnReceived, false, OnDropped, filterOptions) - .WithTimeout(); + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, + new[] { evt }); - // add the events we want to receive after we start the subscription + var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1); + + await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.End, filterOptions: filterOptions); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); + + // add some of the events we want to see after we start the subscription foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - // wait until all events were received and at least one checkpoint was reached? - await receivedAllEvents.Task.WithTimeout(); - await checkpointReached.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, + new[] { evt }); + + bool checkpointReached = false; + + await Subscribe().WithTimeout(); + + Assert.True(checkpointReached); + return; - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - if (availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId) == 0) { - Fixture.Log.Error( - "Received unexpected event {EventId} from {StreamId}", - re.OriginalEvent.EventId, - re.OriginalEvent.EventStreamId - ); - - receivedAllEvents.TrySetException( - new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") - ); - } - else { - Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); - } + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + switch (enumerator.Current) { + case StreamMessage.AllStreamCheckpointReached: + checkpointReached = true; - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); - } + break; + case StreamMessage.Event(var resolvedEvent): { + availableEvents.Remove(resolvedEvent.Event.EventId); - return Task.CompletedTask; - } + if (availableEvents.Count == 0) { + return; + } - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) { - subscriptionDropped.SetResult(new(reason, ex)); - if (reason != SubscriptionDroppedReason.Disposed) { - receivedAllEvents.TrySetException(ex!); - checkpointReached.TrySetException(ex!); + break; + } + } } } - - Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { - Fixture.Log.Verbose( - "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", - position, pageSize - availableEvents.Count, pageSize - ); - checkpointReached.TrySetResult(true); - return Task.CompletedTask; - } } [Theory] - [MemberData(nameof(SubscriptionFilter.TestCases), MemberType= typeof(SubscriptionFilter))] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] public async Task receives_all_filtered_events_from_position(SubscriptionFilter filter) { var streamPrefix = $"{nameof(receives_all_filtered_events_from_position)}-{filter.Name}-{Guid.NewGuid():N}"; - + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); - - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - var checkpointReached = new TaskCompletionSource(); - + var seedEvents = Fixture.CreateTestEvents(64) .Select(evt => filter.PrepareEvent(streamPrefix, evt)) .ToArray(); - + var pageSize = seedEvents.Length / 2; - + // only the second half of the events will be received var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); // add noise - await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); - - var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); + await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, + Fixture.CreateTestEvents(3)); + + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start) + .Messages.CountAsync(); Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); - + // add some of the events that are a match to the filter but will not be received IWriteResult writeResult = new SuccessResult(); foreach (var evt in seedEvents.Take(pageSize)) - writeResult = await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + writeResult = await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", + StreamState.NoStream, new[] { evt }); var position = FromAll.After(writeResult.LogPosition); - - var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(position, OnReceived, false, OnDropped, filterOptions) - .WithTimeout(); + + var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1); + + await using var subscription = Fixture.Streams.SubscribeToAll(position, filterOptions: filterOptions); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); // add the events we want to receive after we start the subscription foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - // wait until all events were received and at least one checkpoint was reached? - await receivedAllEvents.Task.WithTimeout(); - await checkpointReached.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, + new[] { evt }); + + bool checkpointReached = false; + + await Subscribe().WithTimeout(); + + Assert.True(checkpointReached); + return; - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - if (availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId) == 0) { - Fixture.Log.Error( - "Received unexpected event {EventId} from {StreamId}", - re.OriginalEvent.EventId, - re.OriginalEvent.EventStreamId - ); - - receivedAllEvents.TrySetException( - new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") - ); - } - else { - Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); - } + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + switch (enumerator.Current) { + case StreamMessage.AllStreamCheckpointReached: + checkpointReached = true; - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); - } + break; + case StreamMessage.Event(var resolvedEvent): { + availableEvents.Remove(resolvedEvent.Event.EventId); - return Task.CompletedTask; - } + if (availableEvents.Count == 0) { + return; + } - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) { - subscriptionDropped.SetResult(new(reason, ex)); - if (reason != SubscriptionDroppedReason.Disposed) { - receivedAllEvents.TrySetException(ex!); - checkpointReached.TrySetException(ex!); + break; + } + } } } - - Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { - Fixture.Log.Verbose( - "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", - position, pageSize - availableEvents.Count, pageSize - ); - checkpointReached.TrySetResult(true); - return Task.CompletedTask; - } } - + [Fact] public async Task receives_all_filtered_events_with_resolved_links() { var streamName = Fixture.GetStreamName(); - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(3).ToArray(); + var seedEvents = Fixture.CreateTestEvents(3).ToArray(); var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - var options = new SubscriptionFilterOptions( + var filterOptions = new SubscriptionFilterOptions( StreamFilter.Prefix($"$et-{EventStoreFixture.TestEventType}") ); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(FromAll.Start, OnReceived, true, OnDropped, options) - .WithTimeout(); - - await receivedAllEvents.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - + + await using var subscription = + Fixture.Streams.SubscribeToAll(FromAll.Start, true, filterOptions: filterOptions); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); + + await Subscribe().WithTimeout(); + return; - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}"); - if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { - Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); - return Task.CompletedTask; - } - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); - } - - return Task.CompletedTask; - } + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent) || + !resolvedEvent.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}")) { + continue; + } - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); - } - - [Fact] - public async Task drops_when_disposed() { - var subscriptionDropped = new TaskCompletionSource(); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync( - FromAll.Start, - (sub, re, ct) => Task.CompletedTask, - false, - (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) - ) - .WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - } + availableEvents.Remove(resolvedEvent.Event.EventId); - [Fact] - public async Task drops_when_subscriber_error() { - var expectedResult = SubscriptionDroppedResult.SubscriberError(); - - var subscriptionDropped = new TaskCompletionSource(); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync( - FromAll.Start, - (sub, re, ct) => expectedResult.Throw(), - false, - (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) - ) - .WithTimeout(); - - await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents()); - - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(expectedResult); + if (availableEvents.Count == 0) { + return; + } + } + } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all_obsolete.cs b/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all_obsolete.cs new file mode 100644 index 000000000..d64e28c59 --- /dev/null +++ b/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all_obsolete.cs @@ -0,0 +1,592 @@ +namespace EventStore.Client.Streams.Tests.Subscriptions; + +[Trait("Category", "Subscriptions")] +[Trait("Category", "Target:All")] +[Obsolete] +public class subscribe_to_all_obsolete(ITestOutputHelper output, SubscriptionsFixture fixture) : EventStoreTests(output, fixture) { + [Fact] + public async Task receives_all_events_from_start() { + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + var pageSize = seedEvents.Length / 2; + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + foreach (var evt in seedEvents.Take(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(FromAll.Start, OnReceived, false, OnDropped) + .WithTimeout(); + + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Fact] + public async Task receives_all_events_from_end() { + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(FromAll.End, OnReceived, false, OnDropped) + .WithTimeout(); + + // add the events we want to receive after we start the subscription + foreach (var evt in seedEvents) + await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Fact] + public async Task receives_all_events_from_position() { + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + var pageSize = seedEvents.Length / 2; + + // only the second half of the events will be received + var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); + + IWriteResult writeResult = new SuccessResult(); + foreach (var evt in seedEvents.Take(pageSize)) + writeResult = await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + var position = FromAll.After(writeResult.LogPosition); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(position, OnReceived, false, OnDropped) + .WithTimeout(); + + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Fact] + public async Task receives_all_events_with_resolved_links() { + var streamName = Fixture.GetStreamName(); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(3).ToArray(); + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(FromAll.Start, OnReceived, true, OnDropped) + .WithTimeout(); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}"); + if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { + Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); + return Task.CompletedTask; + } + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Theory] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType= typeof(SubscriptionFilter))] + public async Task receives_all_filtered_events_from_start(SubscriptionFilter filter) { + var streamPrefix = $"{nameof(receives_all_filtered_events_from_start)}-{filter.Name}-{Guid.NewGuid():N}"; + + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + var checkpointReached = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(64) + .Select(evt => filter.PrepareEvent(streamPrefix, evt)) + .ToArray(); + + var pageSize = seedEvents.Length / 2; + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + // add noise + await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); + + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); + Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); + + // Debugging: + // await foreach (var evt in Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start)) + // Fixture.Log.Debug("Read event {EventId} from {StreamId}.", evt.OriginalEvent.EventId, evt.OriginalEvent.EventStreamId); + + // add some of the events we want to see before we start the subscription + foreach (var evt in seedEvents.Take(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(FromAll.Start, OnReceived, false, OnDropped, filterOptions) + .WithTimeout(); + + // add some of the events we want to see after we start the subscription + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + // wait until all events were received and at least one checkpoint was reached? + await receivedAllEvents.Task.WithTimeout(); + await checkpointReached.Task.WithTimeout(); + + // await Task.WhenAll(receivedAllEvents.Task, checkpointReached.Task).WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + if (availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId) == 0) { + Fixture.Log.Error( + "Received unexpected event {EventId} from {StreamId}", + re.OriginalEvent.EventId, + re.OriginalEvent.EventStreamId + ); + + receivedAllEvents.TrySetException( + new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") + ); + } + else { + Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}.", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); + } + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events.", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) { + subscriptionDropped.SetResult(new(reason, ex)); + if (reason != SubscriptionDroppedReason.Disposed) { + receivedAllEvents.TrySetException(ex!); + checkpointReached.TrySetException(ex!); + } + } + + Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { + Fixture.Log.Verbose( + "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", + position, seedEvents.Length - availableEvents.Count, seedEvents.Length + ); + checkpointReached.TrySetResult(true); + return Task.CompletedTask; + } + } + + [Theory] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType= typeof(SubscriptionFilter))] + public async Task receives_all_filtered_events_from_end(SubscriptionFilter filter) { + var streamPrefix = $"{nameof(receives_all_filtered_events_from_end)}-{filter.Name}-{Guid.NewGuid():N}"; + + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + var checkpointReached = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(64) + .Select(evt => filter.PrepareEvent(streamPrefix, evt)) + .ToArray(); + + var pageSize = seedEvents.Length / 2; + + // only the second half of the events will be received + var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); + + // add noise + await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); + + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); + Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); + + // add some of the events that are a match to the filter but will not be received + foreach (var evt in seedEvents.Take(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(FromAll.End, OnReceived, false, OnDropped, filterOptions) + .WithTimeout(); + + // add the events we want to receive after we start the subscription + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + // wait until all events were received and at least one checkpoint was reached? + await receivedAllEvents.Task.WithTimeout(); + await checkpointReached.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + if (availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId) == 0) { + Fixture.Log.Error( + "Received unexpected event {EventId} from {StreamId}", + re.OriginalEvent.EventId, + re.OriginalEvent.EventStreamId + ); + + receivedAllEvents.TrySetException( + new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") + ); + } + else { + Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); + } + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) { + subscriptionDropped.SetResult(new(reason, ex)); + if (reason != SubscriptionDroppedReason.Disposed) { + receivedAllEvents.TrySetException(ex!); + checkpointReached.TrySetException(ex!); + } + } + + Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { + Fixture.Log.Verbose( + "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", + position, pageSize - availableEvents.Count, pageSize + ); + checkpointReached.TrySetResult(true); + return Task.CompletedTask; + } + } + + [Theory] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType= typeof(SubscriptionFilter))] + public async Task receives_all_filtered_events_from_position(SubscriptionFilter filter) { + var streamPrefix = $"{nameof(receives_all_filtered_events_from_position)}-{filter.Name}-{Guid.NewGuid():N}"; + + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + var checkpointReached = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(64) + .Select(evt => filter.PrepareEvent(streamPrefix, evt)) + .ToArray(); + + var pageSize = seedEvents.Length / 2; + + // only the second half of the events will be received + var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); + + // add noise + await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); + + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); + Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); + + // add some of the events that are a match to the filter but will not be received + IWriteResult writeResult = new SuccessResult(); + foreach (var evt in seedEvents.Take(pageSize)) + writeResult = await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + var position = FromAll.After(writeResult.LogPosition); + + var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(position, OnReceived, false, OnDropped, filterOptions) + .WithTimeout(); + + // add the events we want to receive after we start the subscription + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + // wait until all events were received and at least one checkpoint was reached? + await receivedAllEvents.Task.WithTimeout(); + await checkpointReached.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + if (availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId) == 0) { + Fixture.Log.Error( + "Received unexpected event {EventId} from {StreamId}", + re.OriginalEvent.EventId, + re.OriginalEvent.EventStreamId + ); + + receivedAllEvents.TrySetException( + new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") + ); + } + else { + Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); + } + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) { + subscriptionDropped.SetResult(new(reason, ex)); + if (reason != SubscriptionDroppedReason.Disposed) { + receivedAllEvents.TrySetException(ex!); + checkpointReached.TrySetException(ex!); + } + } + + Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { + Fixture.Log.Verbose( + "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", + position, pageSize - availableEvents.Count, pageSize + ); + checkpointReached.TrySetResult(true); + return Task.CompletedTask; + } + } + + [Fact] + public async Task receives_all_filtered_events_with_resolved_links() { + var streamName = Fixture.GetStreamName(); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(3).ToArray(); + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + + var options = new SubscriptionFilterOptions( + StreamFilter.Prefix($"$et-{EventStoreFixture.TestEventType}") + ); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(FromAll.Start, OnReceived, true, OnDropped, options) + .WithTimeout(); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}"); + if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { + Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); + return Task.CompletedTask; + } + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Fact] + public async Task drops_when_disposed() { + var subscriptionDropped = new TaskCompletionSource(); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync( + FromAll.Start, + (sub, re, ct) => Task.CompletedTask, + false, + (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) + ) + .WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + } + + [Fact] + public async Task drops_when_subscriber_error() { + var expectedResult = SubscriptionDroppedResult.SubscriberError(); + + var subscriptionDropped = new TaskCompletionSource(); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync( + FromAll.Start, + (sub, re, ct) => expectedResult.Throw(), + false, + (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) + ) + .WithTimeout(); + + await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents()); + + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(expectedResult); + } +} diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream.cs b/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream.cs index ca5dc122e..3cc26c8a1 100644 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream.cs +++ b/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream.cs @@ -2,301 +2,225 @@ namespace EventStore.Client.Streams.Tests.Subscriptions; [Trait("Category", "Subscriptions")] [Trait("Category", "Target:Stream")] -public class subscribe_to_stream(ITestOutputHelper output, SubscriptionsFixture fixture) : EventStoreTests(output, fixture) { +public class subscribe_to_stream(ITestOutputHelper output, SubscriptionsFixture fixture) + : EventStoreTests(output, fixture) { [Fact] public async Task receives_all_events_from_start() { var streamName = Fixture.GetStreamName(); - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - var pageSize = seedEvents.Length / 2; + var pageSize = seedEvents.Length / 2; var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents.Take(pageSize)); - using var subscription = await Fixture.Streams - .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived, false, OnDropped) - .WithTimeout(); + await using var subscription = Fixture.Streams.SubscribeToStream(streamName, FromStream.Start); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.StreamExists, seedEvents.Skip(pageSize)); + Assert.True(await enumerator.MoveNextAsync()); - await receivedAllEvents.Task.WithTimeout(); + Assert.IsType(enumerator.Current); - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.StreamExists, seedEvents.Skip(pageSize)); - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); + await Subscribe().WithTimeout(); return; - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { + continue; + } - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); - } + availableEvents.Remove(resolvedEvent.Event.EventId); - return Task.CompletedTask; + if (availableEvents.Count == 0) { + return; + } + } } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); } [Fact] public async Task receives_all_events_from_position() { var streamName = Fixture.GetStreamName(); - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - var pageSize = seedEvents.Length / 2; + var pageSize = seedEvents.Length / 2; // only the second half of the events will be received var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); - var writeResult = await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents.Take(pageSize)); + var writeResult = + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents.Take(pageSize)); var streamPosition = StreamPosition.FromStreamRevision(writeResult.NextExpectedStreamRevision); - var checkpoint = FromStream.After(streamPosition); + var checkpoint = FromStream.After(streamPosition); - using var subscription = await Fixture.Streams - .SubscribeToStreamAsync(streamName, checkpoint, OnReceived, false, OnDropped) - .WithTimeout(); + await using var subscription = Fixture.Streams.SubscribeToStream(streamName, checkpoint); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - await Fixture.Streams.AppendToStreamAsync(streamName, writeResult.NextExpectedStreamRevision, seedEvents.Skip(pageSize)); + Assert.True(await enumerator.MoveNextAsync()); - await receivedAllEvents.Task.WithTimeout(); + Assert.IsType(enumerator.Current); - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + await Fixture.Streams.AppendToStreamAsync(streamName, writeResult.NextExpectedStreamRevision, + seedEvents.Skip(pageSize)); - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); + await Subscribe().WithTimeout(); return; - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { + continue; + } - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); - } + availableEvents.Remove(resolvedEvent.OriginalEvent.EventId); - return Task.CompletedTask; + if (availableEvents.Count == 0) { + return; + } + } } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); } [Fact] public async Task receives_all_events_from_non_existing_stream() { var streamName = Fixture.GetStreamName(); - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - using var subscription = await Fixture.Streams - .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived, false, OnDropped) - .WithTimeout(); + await using var subscription = Fixture.Streams.SubscribeToStream(streamName, FromStream.Start); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + Assert.True(await enumerator.MoveNextAsync()); - await receivedAllEvents.Task.WithTimeout(); + Assert.IsType(enumerator.Current); - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); + await Subscribe().WithTimeout(); return; - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { + continue; + } - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); - } + availableEvents.Remove(resolvedEvent.OriginalEvent.EventId); - return Task.CompletedTask; + if (availableEvents.Count == 0) { + return; + } + } } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); } [Fact] public async Task allow_multiple_subscriptions_to_same_stream() { var streamName = Fixture.GetStreamName(); - var receivedAllEvents = new TaskCompletionSource(); - var seedEvents = Fixture.CreateTestEvents(5).ToArray(); - var targetEventsCount = seedEvents.Length * 2; - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - using var subscription1 = await Fixture.Streams - .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived) - .WithTimeout(); + await using var subscription1 = Fixture.Streams.SubscribeToStream(streamName, FromStream.Start); + await using var enumerator1 = subscription1.Messages.GetAsyncEnumerator(); - using var subscription2 = await Fixture.Streams - .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived) - .WithTimeout(); + Assert.True(await enumerator1.MoveNextAsync()); - await receivedAllEvents.Task.WithTimeout(); + Assert.IsType(enumerator1.Current); - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - if (--targetEventsCount == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); - } + await using var subscription2 = Fixture.Streams.SubscribeToStream(streamName, FromStream.Start); + await using var enumerator2 = subscription2.Messages.GetAsyncEnumerator(); - return Task.CompletedTask; - } - } + Assert.True(await enumerator2.MoveNextAsync()); - [Fact] - public async Task drops_when_disposed() { - var streamName = Fixture.GetStreamName(); + Assert.IsType(enumerator2.Current); - var subscriptionDropped = new TaskCompletionSource(); - - using var subscription = await Fixture.Streams - .SubscribeToStreamAsync( - streamName, - FromStream.Start, - (sub, re, ct) => Task.CompletedTask, - false, - (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) - ) - .WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - } + await Task.WhenAll(Subscribe(enumerator1), Subscribe(enumerator2)).WithTimeout(); - [Fact] - public async Task drops_when_subscriber_error() { - var streamName = Fixture.GetStreamName(); - - var expectedResult = SubscriptionDroppedResult.SubscriberError(); + return; - var subscriptionDropped = new TaskCompletionSource(); + async Task Subscribe(IAsyncEnumerator subscription) { + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - using var subscription = await Fixture.Streams - .SubscribeToStreamAsync( - streamName, - FromStream.Start, - (sub, re, ct) => expectedResult.Throw(), - false, - (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) - ) - .WithTimeout(); + while (await subscription.MoveNextAsync()) { + if (subscription.Current is not StreamMessage.Event(var resolvedEvent)) { + continue; + } - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, Fixture.CreateTestEvents()); + availableEvents.Remove(resolvedEvent.OriginalEvent.EventId); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(expectedResult); + if (availableEvents.Count == 0) { + return; + } + } + } } [Fact] public async Task drops_when_stream_tombstoned() { var streamName = Fixture.GetStreamName(); - var subscriptionDropped = new TaskCompletionSource(); + await using var subscription = Fixture.Streams.SubscribeToStream(streamName, FromStream.Start); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); - using var subscription = await Fixture.Streams - .SubscribeToStreamAsync( - streamName, - FromStream.Start, - (sub, re, ct) => Task.CompletedTask, - false, - (sub, reason, ex) => { subscriptionDropped.SetResult(new(reason, ex)); } - ) - .WithTimeout(); + Assert.IsType(enumerator.Current); // rest in peace await Fixture.Streams.TombstoneAsync(streamName, StreamState.NoStream); - var result = await subscriptionDropped.Task.WithTimeout(); - result.Error.ShouldBeOfType().Stream.ShouldBe(streamName); + var ex = await Assert.ThrowsAsync(async () => { + while (await enumerator.MoveNextAsync()) { } + }).WithTimeout(); + + ex.ShouldBeOfType().Stream.ShouldBe(streamName); } [Fact] public async Task receives_all_events_with_resolved_links() { var streamName = Fixture.GetStreamName(); - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(3).ToArray(); + var seedEvents = Fixture.CreateTestEvents(3).ToArray(); var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - using var subscription = await Fixture.Streams - .SubscribeToStreamAsync($"$et-{EventStoreFixture.TestEventType}", FromStream.Start, OnReceived, true, OnDropped) - .WithTimeout(); + await using var subscription = + Fixture.Streams.SubscribeToStream($"$et-{EventStoreFixture.TestEventType}", FromStream.Start, true); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - await receivedAllEvents.Task.WithTimeout(); + Assert.True(await enumerator.MoveNextAsync()); - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + Assert.IsType(enumerator.Current); - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); + await Subscribe().WithTimeout(); return; - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}"); - if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { - Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); - return Task.CompletedTask; - } + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent) || + !resolvedEvent.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}")) { + continue; + } - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); - } + availableEvents.Remove(resolvedEvent.Event.EventId); - return Task.CompletedTask; + if (availableEvents.Count == 0) { + return; + } + } } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream_obsolete.cs b/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream_obsolete.cs new file mode 100644 index 000000000..734da73fd --- /dev/null +++ b/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream_obsolete.cs @@ -0,0 +1,303 @@ +namespace EventStore.Client.Streams.Tests.Subscriptions; + +[Trait("Category", "Subscriptions")] +[Trait("Category", "Target:Stream")] +[Obsolete] +public class subscribe_to_stream_obsolete(ITestOutputHelper output, SubscriptionsFixture fixture) : EventStoreTests(output, fixture) { + [Fact] + public async Task receives_all_events_from_start() { + var streamName = Fixture.GetStreamName(); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + var pageSize = seedEvents.Length / 2; + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents.Take(pageSize)); + + using var subscription = await Fixture.Streams + .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived, false, OnDropped) + .WithTimeout(); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.StreamExists, seedEvents.Skip(pageSize)); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Fact] + public async Task receives_all_events_from_position() { + var streamName = Fixture.GetStreamName(); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + var pageSize = seedEvents.Length / 2; + + // only the second half of the events will be received + var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); + + var writeResult = await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents.Take(pageSize)); + var streamPosition = StreamPosition.FromStreamRevision(writeResult.NextExpectedStreamRevision); + var checkpoint = FromStream.After(streamPosition); + + using var subscription = await Fixture.Streams + .SubscribeToStreamAsync(streamName, checkpoint, OnReceived, false, OnDropped) + .WithTimeout(); + + await Fixture.Streams.AppendToStreamAsync(streamName, writeResult.NextExpectedStreamRevision, seedEvents.Skip(pageSize)); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Fact] + public async Task receives_all_events_from_non_existing_stream() { + var streamName = Fixture.GetStreamName(); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + using var subscription = await Fixture.Streams + .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived, false, OnDropped) + .WithTimeout(); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Fact] + public async Task allow_multiple_subscriptions_to_same_stream() { + var streamName = Fixture.GetStreamName(); + + var receivedAllEvents = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(5).ToArray(); + + var targetEventsCount = seedEvents.Length * 2; + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + + using var subscription1 = await Fixture.Streams + .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived) + .WithTimeout(); + + using var subscription2 = await Fixture.Streams + .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived) + .WithTimeout(); + + await receivedAllEvents.Task.WithTimeout(); + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + if (--targetEventsCount == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + } + + [Fact] + public async Task drops_when_disposed() { + var streamName = Fixture.GetStreamName(); + + var subscriptionDropped = new TaskCompletionSource(); + + using var subscription = await Fixture.Streams + .SubscribeToStreamAsync( + streamName, + FromStream.Start, + (sub, re, ct) => Task.CompletedTask, + false, + (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) + ) + .WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + } + + [Fact] + public async Task drops_when_subscriber_error() { + var streamName = Fixture.GetStreamName(); + + var expectedResult = SubscriptionDroppedResult.SubscriberError(); + + var subscriptionDropped = new TaskCompletionSource(); + + using var subscription = await Fixture.Streams + .SubscribeToStreamAsync( + streamName, + FromStream.Start, + (sub, re, ct) => expectedResult.Throw(), + false, + (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) + ) + .WithTimeout(); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, Fixture.CreateTestEvents()); + + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(expectedResult); + } + + [Fact] + public async Task drops_when_stream_tombstoned() { + var streamName = Fixture.GetStreamName(); + + var subscriptionDropped = new TaskCompletionSource(); + + using var subscription = await Fixture.Streams + .SubscribeToStreamAsync( + streamName, + FromStream.Start, + (sub, re, ct) => Task.CompletedTask, + false, + (sub, reason, ex) => { subscriptionDropped.SetResult(new(reason, ex)); } + ) + .WithTimeout(); + + // rest in peace + await Fixture.Streams.TombstoneAsync(streamName, StreamState.NoStream); + + var result = await subscriptionDropped.Task.WithTimeout(); + result.Error.ShouldBeOfType().Stream.ShouldBe(streamName); + } + + [Fact] + public async Task receives_all_events_with_resolved_links() { + var streamName = Fixture.GetStreamName(); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(3).ToArray(); + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + + using var subscription = await Fixture.Streams + .SubscribeToStreamAsync($"$et-{EventStoreFixture.TestEventType}", FromStream.Start, OnReceived, true, OnDropped) + .WithTimeout(); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}"); + if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { + Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); + return Task.CompletedTask; + } + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } +} diff --git a/test/EventStore.Client.Tests.Common/Extensions/TaskExtensions.cs b/test/EventStore.Client.Tests.Common/Extensions/TaskExtensions.cs index b02d5ef2a..d3775b700 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/TaskExtensions.cs +++ b/test/EventStore.Client.Tests.Common/Extensions/TaskExtensions.cs @@ -26,4 +26,4 @@ public static async Task WithTimeout(this Task task, int timeoutMs = 15 throw new TimeoutException(message ?? "Timed out waiting for task"); } -} \ No newline at end of file +}