Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ClientModel: AsyncResultCollection<T> and SSE event collection implementation #43840

Merged
merged 53 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
54f822b
Add async and sync enumerable client results
annelo-msft Apr 12, 2024
d239e17
Implement IAsyncDisposable
annelo-msft Apr 15, 2024
115295a
Merge remote-tracking branch 'upstream/main' into clientmodel-asyncen…
annelo-msft Apr 19, 2024
3b12f1f
Remove constrant and disposable; update ClientResult so response can …
annelo-msft Apr 19, 2024
dbf18ad
rename and upate tests
annelo-msft Apr 20, 2024
a798fd9
Merge remote-tracking branch 'upstream/main' into clientmodel-asyncen…
annelo-msft May 1, 2024
72951b9
Merge remote-tracking branch 'upstream/main' into clientmodel-asyncen…
annelo-msft May 2, 2024
63e1351
initial addition of files from https://github.com/joseharriaga/openai…
annelo-msft May 2, 2024
f604598
make it build
annelo-msft May 2, 2024
d3bd6b8
hello world test
annelo-msft May 2, 2024
3fca028
bootstrap more tests
annelo-msft May 2, 2024
4ea146d
more internal tests
annelo-msft May 2, 2024
262c78d
adding enumerator tests; haven't figured out the batch piece yet
annelo-msft May 2, 2024
984499d
Make batch test pass
annelo-msft May 2, 2024
f26a89b
remove collection-event functionality and add tests for public type
annelo-msft May 3, 2024
647f1a9
reshuffle
annelo-msft May 3, 2024
3189fb3
Add mock convenience SSE type to give POC of lazy request sending
annelo-msft May 3, 2024
1a0c861
add tests of delayed request
annelo-msft May 3, 2024
d5cbc8e
Add BinaryData factory method
annelo-msft May 3, 2024
2d71562
Merge remote-tracking branch 'upstream/main' into clientmodel-sse-nod…
annelo-msft May 6, 2024
0beaeea
remove funcs for creating enumerators
annelo-msft May 6, 2024
fc84db1
renames
annelo-msft May 6, 2024
5a1bd13
postpone call to protocol method from convenience APIs
annelo-msft May 6, 2024
75e3e8a
implement IAsyncDisposable correctly
annelo-msft May 6, 2024
80e6ee5
initial pass over cancellation token
annelo-msft May 6, 2024
c27581d
Per FDG, throw OperationCanceledException if cancellation token is ca…
annelo-msft May 6, 2024
a04f543
remove factory method taking Func<T> and provide example of layering …
annelo-msft May 6, 2024
d6d4375
rename internal types and WIP adding reader tests
annelo-msft May 7, 2024
6fa96cb
nits
annelo-msft May 7, 2024
1fbd038
parameterize terminal event; TBD to provide virtual method on collect…
annelo-msft May 7, 2024
f577d2e
Merge remote-tracking branch 'upstream/main' into clientmodel-sse-nod…
annelo-msft May 7, 2024
f342162
WIP: nits
annelo-msft May 7, 2024
4ac87a2
WIP: added concatenation of data lines per SSE spec
annelo-msft May 8, 2024
dab0852
updates and bug fixes
annelo-msft May 8, 2024
1941f2c
add tests and update per SSE spec
annelo-msft May 8, 2024
dee2173
Merge remote-tracking branch 'upstream/main' into clientmodel-sse-nod…
annelo-msft May 8, 2024
58ac2a3
WIP: refactor to reuse field processing across sync and async methods
annelo-msft May 9, 2024
20a3b81
Merge remote-tracking branch 'upstream/main' into clientmodel-sse-nod…
annelo-msft May 9, 2024
88d5da1
make look a little more like the BCL type proposal
annelo-msft May 9, 2024
d2776d8
simplify field implementation a bit
annelo-msft May 9, 2024
2758377
cosmetic reworking of creating an event from a pending event
annelo-msft May 9, 2024
eae0caa
Remove factory method from public API; move MockSseClient to Tests.In…
annelo-msft May 9, 2024
b8f70f7
update API; reimplement mock client implementations without internal …
annelo-msft May 9, 2024
a592c2c
Add sync client result collection abstraction
annelo-msft May 9, 2024
88de121
tidy up and add tests
annelo-msft May 10, 2024
3bf665b
add default constructor to ClientResult
annelo-msft May 10, 2024
f87933e
more tidy-up
annelo-msft May 10, 2024
43dbbc2
rename and add refdocs
annelo-msft May 10, 2024
caebaea
comments
annelo-msft May 10, 2024
67392c1
Merge remote-tracking branch 'upstream/main' into clientmodel-sse-nod…
annelo-msft May 14, 2024
727185c
pr fb
annelo-msft May 14, 2024
df2df9e
rework last event id and retry per BCL design shift
annelo-msft May 14, 2024
af84592
add CHANGELOG entry
annelo-msft May 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sdk/core/Azure.Core.TestFramework/src/MockJsonModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Azure.Core.TestFramework
{
public class MockJsonModel : IJsonModel<MockJsonModel>
{
internal MockJsonModel()
public MockJsonModel()
{
}

Expand Down
16 changes: 15 additions & 1 deletion sdk/core/System.ClientModel/api/System.ClientModel.net6.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ public ApiKeyCredential(string key) { }
public static implicit operator System.ClientModel.ApiKeyCredential (string key) { throw null; }
public void Update(string key) { }
}
public abstract partial class AsyncClientResultCollection<T> : System.ClientModel.ClientResult, System.Collections.Generic.IAsyncEnumerable<T>
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
{
protected internal AsyncClientResultCollection() : base (default(System.ClientModel.Primitives.PipelineResponse)) { }
protected internal AsyncClientResultCollection(System.ClientModel.Primitives.PipelineResponse response) : base (default(System.ClientModel.Primitives.PipelineResponse)) { }
public abstract System.Collections.Generic.IAsyncEnumerator<T> GetAsyncEnumerator(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
}
public abstract partial class BinaryContent : System.IDisposable
{
protected BinaryContent() { }
Expand All @@ -20,11 +26,19 @@ protected BinaryContent() { }
}
public partial class ClientResult
{
protected ClientResult(System.ClientModel.Primitives.PipelineResponse response) { }
protected ClientResult(System.ClientModel.Primitives.PipelineResponse? response) { }
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
public static System.ClientModel.ClientResult<T?> FromOptionalValue<T>(T? value, System.ClientModel.Primitives.PipelineResponse response) { throw null; }
public static System.ClientModel.ClientResult FromResponse(System.ClientModel.Primitives.PipelineResponse response) { throw null; }
public static System.ClientModel.ClientResult<T> FromValue<T>(T value, System.ClientModel.Primitives.PipelineResponse response) { throw null; }
public System.ClientModel.Primitives.PipelineResponse GetRawResponse() { throw null; }
protected void SetRawResponse(System.ClientModel.Primitives.PipelineResponse response) { }
}
public abstract partial class ClientResultCollection<T> : System.ClientModel.ClientResult, System.Collections.Generic.IEnumerable<T>, System.Collections.IEnumerable
{
protected internal ClientResultCollection() : base (default(System.ClientModel.Primitives.PipelineResponse)) { }
protected internal ClientResultCollection(System.ClientModel.Primitives.PipelineResponse response) : base (default(System.ClientModel.Primitives.PipelineResponse)) { }
public abstract System.Collections.Generic.IEnumerator<T> GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
}
public partial class ClientResultException : System.Exception
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ public ApiKeyCredential(string key) { }
public static implicit operator System.ClientModel.ApiKeyCredential (string key) { throw null; }
public void Update(string key) { }
}
public abstract partial class AsyncClientResultCollection<T> : System.ClientModel.ClientResult, System.Collections.Generic.IAsyncEnumerable<T>
{
protected internal AsyncClientResultCollection() : base (default(System.ClientModel.Primitives.PipelineResponse)) { }
protected internal AsyncClientResultCollection(System.ClientModel.Primitives.PipelineResponse response) : base (default(System.ClientModel.Primitives.PipelineResponse)) { }
public abstract System.Collections.Generic.IAsyncEnumerator<T> GetAsyncEnumerator(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
}
public abstract partial class BinaryContent : System.IDisposable
{
protected BinaryContent() { }
Expand All @@ -20,11 +26,19 @@ protected BinaryContent() { }
}
public partial class ClientResult
{
protected ClientResult(System.ClientModel.Primitives.PipelineResponse response) { }
protected ClientResult(System.ClientModel.Primitives.PipelineResponse? response) { }
public static System.ClientModel.ClientResult<T?> FromOptionalValue<T>(T? value, System.ClientModel.Primitives.PipelineResponse response) { throw null; }
public static System.ClientModel.ClientResult FromResponse(System.ClientModel.Primitives.PipelineResponse response) { throw null; }
public static System.ClientModel.ClientResult<T> FromValue<T>(T value, System.ClientModel.Primitives.PipelineResponse response) { throw null; }
public System.ClientModel.Primitives.PipelineResponse GetRawResponse() { throw null; }
protected void SetRawResponse(System.ClientModel.Primitives.PipelineResponse response) { }
}
public abstract partial class ClientResultCollection<T> : System.ClientModel.ClientResult, System.Collections.Generic.IEnumerable<T>, System.Collections.IEnumerable
{
protected internal ClientResultCollection() : base (default(System.ClientModel.Primitives.PipelineResponse)) { }
protected internal ClientResultCollection(System.ClientModel.Primitives.PipelineResponse response) : base (default(System.ClientModel.Primitives.PipelineResponse)) { }
public abstract System.Collections.Generic.IEnumerator<T> GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
}
public partial class ClientResultException : System.Exception
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Threading;

namespace System.ClientModel;

#pragma warning disable CS1591 // public XML comments
public abstract class AsyncClientResultCollection<T> : ClientResult, IAsyncEnumerable<T>
{
// Constructor overload for collection implementations that postpone
// sending a request until GetAsyncEnumerator is called. This will typically
// be used by collections returned from client convenience methods.
protected internal AsyncClientResultCollection() : base(default)
{
}

// Constructor overload for collection implementations where the service
// has returned a response. This will typically be used by collections
// created from the return result of a client's protocol method.
protected internal AsyncClientResultCollection(PipelineResponse response) : base(response)
{
}

public abstract IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
#pragma warning restore CS1591 // public XML comments
54 changes: 47 additions & 7 deletions sdk/core/System.ClientModel/src/Convenience/ClientResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,16 @@ namespace System.ClientModel;
/// </summary>
public class ClientResult
{
private readonly PipelineResponse _response;
private PipelineResponse? _response;

/// <summary>
/// Create a new instance of <see cref="ClientResult"/> from a service
/// response.
/// </summary>
/// <param name="response">The <see cref="PipelineResponse"/> received
/// from the service.</param>
protected ClientResult(PipelineResponse response)
protected ClientResult(PipelineResponse? response)
{
Argument.AssertNotNull(response, nameof(response));

_response = response;
}

Expand All @@ -31,7 +29,39 @@ protected ClientResult(PipelineResponse response)
/// </summary>
/// <returns>the <see cref="PipelineResponse"/> received from the service.
/// </returns>
public PipelineResponse GetRawResponse() => _response;
/// <exception cref="InvalidOperationException">No
/// <see cref="PipelineResponse"/> value is currently available for this
/// <see cref="ClientResult"/> instance. This can happen when the instance
/// is a collection type like <see cref="AsyncClientResultCollection{T}"/>
/// that has not yet been enumerated.</exception>
public PipelineResponse GetRawResponse()
{
if (_response is null)
{
throw new InvalidOperationException("No response is associated " +
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
"with this result. If the result is a collection result " +
"type, this may be because no request has been sent to the " +
"server yet.");
}

return _response;
}

/// <summary>
/// Update the value returned from <see cref="GetRawResponse"/>.
/// </summary>
/// <remarks>This method may be called from types derived from
/// <see cref="ClientResult"/> that poll the service for status updates
/// or to retrieve additional collection values to update the raw response
/// to the response most recently returned from the service.</remarks>
/// <param name="response">The <see cref="PipelineResponse"/> to return
/// from <see cref="GetRawResponse"/>.</param>
protected void SetRawResponse(PipelineResponse response)
{
Argument.AssertNotNull(response, nameof(response));

_response = response;
}

#region Factory methods for ClientResult and subtypes

Expand All @@ -44,7 +74,11 @@ protected ClientResult(PipelineResponse response)
/// provided <paramref name="response"/>.
/// </returns>
public static ClientResult FromResponse(PipelineResponse response)
=> new ClientResult(response);
{
Argument.AssertNotNull(response, nameof(response));

return new ClientResult(response);
}

/// <summary>
/// Creates a new instance of <see cref="ClientResult{T}"/> that holds the
Expand All @@ -60,6 +94,8 @@ public static ClientResult FromResponse(PipelineResponse response)
/// </returns>
public static ClientResult<T> FromValue<T>(T value, PipelineResponse response)
{
Argument.AssertNotNull(response, nameof(response));

if (value is null)
{
string message = "ClientResult<T> contract guarantees that ClientResult<T>.Value is non-null. " +
Expand Down Expand Up @@ -90,7 +126,11 @@ public static ClientResult<T> FromValue<T>(T value, PipelineResponse response)
/// provided <paramref name="value"/> and <paramref name="response"/>.
/// </returns>
public static ClientResult<T?> FromOptionalValue<T>(T? value, PipelineResponse response)
=> new ClientResult<T?>(value, response);
{
Argument.AssertNotNull(response, nameof(response));

return new ClientResult<T?>(value, response);
}

#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.ClientModel.Primitives;
using System.Collections;
using System.Collections.Generic;

namespace System.ClientModel;

#pragma warning disable CS1591 // public XML comments
public abstract class ClientResultCollection<T> : ClientResult, IEnumerable<T>
{
// Constructor overload for collection implementations that postpone
// sending a request until GetAsyncEnumerator is called. This will typically
// be used by collections returned from client convenience methods.
protected internal ClientResultCollection() : base(default)
{
}

// Constructor overload for collection implementations where the service
// has returned a response. This will typically be used by collections
// created from the return result of a client's protocol method.
protected internal ClientResultCollection(PipelineResponse response) : base(response)
{
}

public abstract IEnumerator<T> GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
#pragma warning restore CS1591 // public XML comments
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace System.ClientModel.Internal;

internal class AsyncServerSentEventEnumerable : IAsyncEnumerable<ServerSentEvent>
{
private readonly Stream _contentStream;

public AsyncServerSentEventEnumerable(Stream contentStream)
{
_contentStream = contentStream;
}

public IAsyncEnumerator<ServerSentEvent> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
return new AsyncServerSentEventEnumerator(_contentStream, cancellationToken);
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
}

private sealed class AsyncServerSentEventEnumerator : IAsyncEnumerator<ServerSentEvent>
{
private readonly CancellationToken _cancellationToken;
private readonly ServerSentEventReader _reader;

public ServerSentEvent Current { get; private set; }

public AsyncServerSentEventEnumerator(Stream contentStream, CancellationToken cancellationToken = default)
{
_reader = new(contentStream);
_cancellationToken = cancellationToken;
}

public async ValueTask<bool> MoveNextAsync()
{
ServerSentEvent? nextEvent = await _reader.TryGetNextEventAsync(_cancellationToken).ConfigureAwait(false);

if (nextEvent.HasValue)
{
Current = nextEvent.Value;
return true;
}

Current = default;
return false;
}

public ValueTask DisposeAsync()
{
// The creator of the enumerable has responsibility for disposing
// the content stream passed to the enumerable constructor.

#if NET6_0_OR_GREATER
return ValueTask.CompletedTask;
#else
return new ValueTask();
#endif
}
}
}
30 changes: 30 additions & 0 deletions sdk/core/System.ClientModel/src/Internal/SSE/ServerSentEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace System.ClientModel.Internal;

// SSE specification: https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream
internal readonly struct ServerSentEvent
{
// Gets the value of the SSE "event type" buffer, used to distinguish between event kinds.
public string EventType { get; }

// Gets the value of the SSE "data" buffer, which holds the payload of the server-sent event.
public string Data { get; }

// Gets the value of the "last event ID" buffer, with which a user agent can reestablish a session.
public string? Id { get; }

// If present, gets the defined "retry" value for the event, which represents the delay before reconnecting.
public TimeSpan? ReconnectionTime { get; }
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved

public ServerSentEvent(string type, string data, string? id, string? retry)
{
EventType = type;
Data = data;
Id = id;
ReconnectionTime = retry is null ?
default :
int.TryParse(retry, out int time) ? TimeSpan.FromMilliseconds(time) : null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections;
using System.Collections.Generic;
using System.IO;

namespace System.ClientModel.Internal;

internal class ServerSentEventEnumerable : IEnumerable<ServerSentEvent>
{
private readonly Stream _contentStream;

public ServerSentEventEnumerable(Stream contentStream)
{
_contentStream = contentStream;
}

public IEnumerator<ServerSentEvent> GetEnumerator()
{
return new ServerSentEventEnumerator(_contentStream);
}

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

private sealed class ServerSentEventEnumerator : IEnumerator<ServerSentEvent>
{
private readonly ServerSentEventReader _reader;

public ServerSentEventEnumerator(Stream contentStream)
{
_reader = new(contentStream);
}

public ServerSentEvent Current { get; private set; }

object IEnumerator.Current => Current;

public bool MoveNext()
{
ServerSentEvent? nextEvent = _reader.TryGetNextEvent();

if (nextEvent.HasValue)
{
Current = nextEvent.Value;
return true;
}

Current = default;
return false;
}

public void Reset()
{
throw new NotSupportedException("Cannot seek back in an SSE stream.");
}

public void Dispose()
{
// The creator of the enumerable has responsibility for disposing
// the content stream passed to the enumerable constructor.
}
}
}
Loading