- 
                Notifications
    You must be signed in to change notification settings 
- Fork 18
[ADR] RPC streaming design doc #952
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
          
     Open
      
      
            timtay-microsoft
  wants to merge
  80
  commits into
  main
  
    
      
        
          
  
    
      Choose a base branch
      
     
    
      
        
      
      
        
          
          
        
        
          
            
              
              
              
  
           
        
        
          
            
              
              
           
        
       
     
  
        
          
            
          
            
          
        
       
    
      
from
timtay/streaming
  
      
      
   
  
    
  
  
  
 
  
      
    base: main
Could not load branches
            
              
  
    Branch not found: {{ refName }}
  
            
                
      Loading
              
            Could not load tags
            
            
              Nothing to show
            
              
  
            
                
      Loading
              
            Are you sure you want to change the base?
            Some commits from the old base branch may be removed from the timeline,
            and old review comments may become outdated.
          
          
  
     Open
                    Changes from 12 commits
      Commits
    
    
            Show all changes
          
          
            80 commits
          
        
        Select commit
          Hold shift + click to select a range
      
      eb975a4
              
                asdf
              
              
                timtay-microsoft 4d27162
              
                noodling so far
              
              
                timtay-microsoft 0163c76
              
                sadf
              
              
                timtay-microsoft 73b12d7
              
                maybe
              
              
                timtay-microsoft 4080104
              
                doc
              
              
                timtay-microsoft 4ac4003
              
                more
              
              
                timtay-microsoft af7c829
              
                asdf
              
              
                timtay-microsoft 215eb64
              
                asdf
              
              
                timtay-microsoft 615ba26
              
                thoughts
              
              
                timtay-microsoft ad17af3
              
                more
              
              
                timtay-microsoft a2c2f6e
              
                notes
              
              
                timtay-microsoft 6bf1bcc
              
                caching
              
              
                timtay-microsoft 180d85e
              
                more thoughts
              
              
                timtay-microsoft 599a431
              
                Merge branch 'main' into timtay/streaming
              
              
                timtay-microsoft a1d2e21
              
                no new error code, some gRPC notes
              
              
                timtay-microsoft e680263
              
                save impl for later
              
              
                timtay-microsoft 629dc2f
              
                ordering q
              
              
                timtay-microsoft f2a2c60
              
                wording
              
              
                timtay-microsoft 710a425
              
                links
              
              
                timtay-microsoft 0006c02
              
                backwards
              
              
                timtay-microsoft b457888
              
                first thoughts on cancellation, re-order doc a bit
              
              
                timtay-microsoft f16ad43
              
                more notes, more re-ordering
              
              
                timtay-microsoft e7c98d5
              
                cleanup
              
              
                timtay-microsoft 1c1964b
              
                Only allow cancelling streaming commands
              
              
                timtay-microsoft edecd39
              
                Merge branch 'main' into timtay/streaming
              
              
                timtay-microsoft 61a5212
              
                typo
              
              
                timtay-microsoft 79af301
              
                code changes in another branch
              
              
                timtay-microsoft c7fb69e
              
                more
              
              
                timtay-microsoft aa2dc81
              
                more
              
              
                timtay-microsoft 0784a10
              
                Update ExtendedResponse.cs
              
              
                timtay-microsoft ff9efc9
              
                Update AkriSystemProperties.cs
              
              
                timtay-microsoft 0ff4dbe
              
                Update 0025-rpc-streaming.md
              
              
                timtay-microsoft 4aef0a4
              
                Remove responseId concept. User will do this with their own user prop…
              
              
                timtay-microsoft 9dbb174
              
                Incorporate a lot of feedback
              
              
                timtay-microsoft 49b57c7
              
                cleanup
              
              
                timtay-microsoft ac799bc
              
                canceled error code instead of header
              
              
                timtay-microsoft 04f1ce9
              
                timeout thoughts
              
              
                timtay-microsoft 14ca259
              
                Merge branch 'main' into timtay/streaming
              
              
                timtay-microsoft 353a2fd
              
                asdf
              
              
                timtay-microsoft 439c5f8
              
                Merge branch 'timtay/streaming' of https://github.com/Azure/iot-opera…
              
              
                timtay-microsoft e462a3f
              
                reword
              
              
                timtay-microsoft 4c4cb0b
              
                API fix
              
              
                timtay-microsoft 5050ada
              
                not needed
              
              
                timtay-microsoft 9caa59d
              
                non-req
              
              
                timtay-microsoft 58d8fc1
              
                fix type
              
              
                timtay-microsoft fead2ad
              
                timeout musings
              
              
                timtay-microsoft 7c83f06
              
                wording
              
              
                timtay-microsoft fb9de69
              
                Update 0025-rpc-streaming.md
              
              
                timtay-microsoft 29e1811
              
                Update 0025-rpc-streaming.md
              
              
                timtay-microsoft c15790c
              
                Update 0025-rpc-streaming.md
              
              
                timtay-microsoft 98f8763
              
                Update 0025-rpc-streaming.md
              
              
                timtay-microsoft bf8c252
              
                Address feedback
              
              
                timtay-microsoft 8e6a6d8
              
                note
              
              
                timtay-microsoft 59afded
              
                more
              
              
                timtay-microsoft 69d7781
              
                More, complete streams at any time
              
              
                timtay-microsoft 4a5b22d
              
                ExecutorId is mandatory
              
              
                timtay-microsoft 91f55f3
              
                fix .NET APIs
              
              
                timtay-microsoft 0c7fd4a
              
                disconnection considerations
              
              
                timtay-microsoft 66556a5
              
                De-duping?
              
              
                timtay-microsoft 6b15e19
              
                Revert "De-duping?"
              
              
                timtay-microsoft bddf3f9
              
                executorId in tests
              
              
                timtay-microsoft f5dff71
              
                Merge branch 'main' into timtay/streaming
              
              
                timtay-microsoft c39c41a
              
                fix
              
              
                timtay-microsoft 6c3f1ec
              
                Merge branch 'timtay/streaming' of https://github.com/Azure/iot-opera…
              
              
                timtay-microsoft abe8ae8
              
                No more executor Id, just use $partition
              
              
                timtay-microsoft e6cef75
              
                de-dup + qos 1 clarification
              
              
                timtay-microsoft f4929a2
              
                fixup
              
              
                timtay-microsoft 8322019
              
                Optionally delay acknowledgements
              
              
                timtay-microsoft 1c6c3ad
              
                cancellation user properties so far
              
              
                timtay-microsoft e8d75e4
              
                more cancellation user properties support
              
              
                timtay-microsoft 645f306
              
                message level timeout is back
              
              
                timtay-microsoft 3e0f863
              
                fixup
              
              
                timtay-microsoft 886906c
              
                fixup
              
              
                timtay-microsoft 040940d
              
                timeout vs cancellation
              
              
                timtay-microsoft c7cba20
              
                more
              
              
                timtay-microsoft 8f72c15
              
                isLast
              
              
                timtay-microsoft 5c5d4b3
              
                unused
              
              
                timtay-microsoft 1e59bd9
              
                expiry interval note
              
              
                timtay-microsoft 266a809
              
                message expiry purpose
              
              
                timtay-microsoft 10f55dc
              
                broker behavior
              
              
                timtay-microsoft File filter
Filter by extension
Conversations
          Failed to load comments.   
        
        
          
      Loading
        
  Jump to
        
          Jump to file
        
      
      
          Failed to load files.   
        
        
          
      Loading
        
  Diff view
Diff view
          Some comments aren't visible on the classic Files Changed page.
        
There are no files selected for viewing
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| # ADR 25: RPC Streaming | ||
|  | ||
| ## Context | ||
|  | ||
| Users have expressed a desire to allow more than one response per RPC invocation. This would enable scenarios like: | ||
|  | ||
| - Execute long-running commands while still being responsive | ||
| - | ||
|  | ||
| ## Requirements | ||
| - Allow for an arbitrary number of command responses for a single command invocation | ||
| - The total number of responses does not need to be known before the first response is sent | ||
| - When exposed to the user, each response includes an index of where it was in the stream | ||
| - Allow for multiple separate commands to be streamed simultaneously | ||
| - Even the same command can be executed in parallel to itself? | ||
|  | ||
| ## Non-requirements | ||
| - Different payload shapes per command response | ||
|  | ||
| ## State of the art | ||
|  | ||
| What does gRPC do? | ||
|  | ||
| ## Decision | ||
|  | ||
| Our command invoker base class will now include a new method ```InvokeCommandWithStreaming``` to go with the existing ```InvokeCommand``` method. | ||
|  | ||
| This new method will take the same parameters as ```InvokeCommand``` but will return an asynchronously iterable list (or callback depending on language?) of command response objects. | ||
|  | ||
| ```csharp | ||
| public abstract class CommandInvoker<TReq, TResp> | ||
| where TReq : class | ||
| where TResp : class | ||
| { | ||
| // Single response | ||
| public Task<ExtendedResponse<TResp>> InvokeCommandAsync(TReq request, ...) {...} | ||
|  | ||
| // Many responses, responses may be staggered | ||
| public IAsyncEnumerable<StreamingExtendedResponse<TResp>> InvokeStreamingCommandAsync(TReq request, ...) {...} | ||
| } | ||
| ``` | ||
|  | ||
| Additionally, this new method will return an extended version of the ```ExtendedResponse``` wrapper that will include the streaming-specific information about each response: | ||
|  | ||
| ```csharp | ||
| public class StreamingExtendedResponse<TResp> : ExtendedResponse<TResp> | ||
| where TResp : class | ||
| { | ||
| /// <summary> | ||
| /// An optional Id for this response (relative to the other responses in this response stream) | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Users are allowed to provide Ids for each response, only for specific responses, or for none of the responses. | ||
| /// </remarks> | ||
| public string? StreamingResponseId { get; set; } | ||
|  | ||
| /// <summary> | ||
| /// The index of this response relative to the other responses in this response stream. Starts at 0. | ||
| /// </summary> | ||
| public int StreamingResponseIndex { get; set; } | ||
|  | ||
| /// <summary> | ||
| /// If true, this response is the final response in this response stream. | ||
| /// </summary> | ||
| public bool IsLastResponse { get; set; } | ||
| } | ||
| ``` | ||
|  | ||
| On the executor side, we will define a separate callback that executes whenever a streaming command is invoked. Instead of returning the single response, this callback will return the asynchronously iterable list of responses. Importantly, this iterable may still be added to by the user after this callback has finished. | ||
|  | ||
| ```csharp | ||
| public abstract class CommandExecutor<TReq, TResp> : IAsyncDisposable | ||
| where TReq : class | ||
| where TResp : class | ||
| { | ||
| /// <summary> | ||
| /// The callback to execute each time a non-streaming command request is received. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// This callback may be null if this command executor only supports commands that stream responses. | ||
| /// </remarks> | ||
| public Func<ExtendedRequest<TReq>, CancellationToken, Task<ExtendedResponse<TResp>>>? OnCommandReceived { get; set; } | ||
|  | ||
| /// <summary> | ||
| /// The callback to execute each time a command request that expects streamed responses is received. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The callback provides the request itself and requires the user to return one to many responses. This callback may be null | ||
| /// if this command executors doesn't have any streaming commands. | ||
| /// </remarks> | ||
| public Func<ExtendedRequest<TReq>, CancellationToken, Task<IAsyncEnumerable<StreamingExtendedResponse<TResp>>>>? OnStreamingCommandReceived { get; set; } | ||
| } | ||
|  | ||
| ``` | ||
|  | ||
| With this design, commands that use streaming are defined at codegen time. Codegen layer changes will be defined in a separate ADR, though. | ||
|  | ||
| ## Example with code gen | ||
|  | ||
| TODO which existing client works well for long-running commands? Mem mon ("Report usage for 10 seconds at 1 second intervals")? | ||
|  | ||
| ### MQTT layer implementation | ||
|  | ||
| #### Command invoker side | ||
|  | ||
| - The command invoker's request message will include an MQTT user property with name "__isStream" and value "true". | ||
| - Otherwise, the request message will look the same as a non-streaming RPC request | ||
| - The command invoker will listen for command responses with the correlation data that matches the invoked method's correlation data until it receives a response with the "__isLastResp" flag | ||
| - The command invoker will acknowledge all messages it receives that match the correlation data of the command request | ||
|  | ||
| #### Command executor side | ||
|  | ||
| - All command responses will use the same MQTT message correlation data as the request provided so that the invoker can map responses to the appropriate command invocation. | ||
| - Each streamed response will contain an MQTT user property with name "__streamRespId" and value equal to that response's streaming response Id. | ||
| - The final command response will include an MQTT user property "__isLastResp" with value "true" to signal that it is the final response in the stream. | ||
| - A streaming command is allowed to have a single response. If the stream only has one response, it should include both the "__isStream" and "__isLastResp" flags set. | ||
| - All **completed** streamed command responses will be added to the command response cache | ||
| - If we cache incompleted commands, will the cache hit just wait on cache additions to get the remaining responses? | ||
| - Cache exists for de-duplication, and we want that even for long-running RPC, right? | ||
| - Separate cache for data structure purposes? | ||
|  | ||
| ### Protocol version update | ||
|  | ||
| This feature is not backwards compatible (old invoker can't communicate with new executor that may try to stream a response), so it requires a bump in our RPC protocol version from "1.0" to "2.0". | ||
|  | ||
| TODO: Start defining a doc in our repo that defines what features are present in what protocol version. | ||
|  | ||
| ## Alternative designs considered | ||
|  | ||
| - Allow the command executor to decide at run time of each command if it will stream responses | ||
| - This would force users to call the ```InvokeCommandWithStreaming``` API on the command invoker side and that returned object isn't as easy to use for single responses | ||
| - Treat streaming RPC as a separate protocol from RPC, give it its own client like ```CommandInvoker``` and ```TelemetrySender``` | ||
| - There is a lot of code re-use between RPC and streaming RPC so this would make implementation very inconvenient | ||
| - This would introduce another protocol to version. Future RPC changes would likely be relevant to RPC streaming anyways, so this feels redundant. | ||
|  | ||
| ## Error cases | ||
|  | ||
| - RPC executor dies after sending X out of Y responses. Just time out waiting on X+1'th reply? | ||
| - RPC executor doesn't support streaming but receives a streaming request | ||
| - RPC executor responds with "NotSupportedVersion" error code | ||
| - RPC invoker tries to invoke a command that the executor requires streaming on | ||
| - timeout per response vs overall? | ||
|  | ||
|         
                  timtay-microsoft marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| ## Open Questions | ||
|  | ||
| - Do we need to include response index user property on each streamed response? | ||
|         
                  timtay-microsoft marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| - MQTT message ordering suggests this information can just be inferred by the command invoker | ||
| - Command timeout/cancellation tokens in single vs streaming? | ||
| - When to ack the streaming request? | ||
| - In normal RPC, request is Ack'd only after the method finishes invocation, but this would likely clog up Acks since streaming requests can take a while. | ||
| - Ack after first response is generated? | ||
|         
                  timtay-microsoft marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
        
          
          
            97 changes: 97 additions & 0 deletions
          
          97 
        
  dotnet/src/Azure.Iot.Operations.Protocol/RPC/BlockingConcurrentQueue.cs
  
  
      
      
   
        
      
      
    
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|  | ||
| using Azure.Iot.Operations.Protocol; | ||
| using System; | ||
| using System.Collections.Concurrent; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
|  | ||
| namespace Azure.Iot.Operations.Protocol.RPC | ||
| { | ||
| /// <summary> | ||
| /// A blocking queue that is thread safe. | ||
| /// </summary> | ||
| /// <typeparam name="T">The type of all the elements in the blocking queue.</typeparam> | ||
| /// <remarks>Note that this is a copy of the "BlockingConcurrentDelayableQueue" defined in the MQTT package, but without the "delayable" feature</remarks> | ||
| internal class BlockingConcurrentQueue<T> : IDisposable | ||
| { | ||
| private readonly ConcurrentQueue<T> _queue; | ||
| private readonly SemaphoreSlim _gate; | ||
|  | ||
| public BlockingConcurrentQueue() | ||
| { | ||
| _queue = new ConcurrentQueue<T>(); | ||
| _gate = new(0, 1); | ||
| } | ||
|  | ||
| /// <summary> | ||
| /// Delete all entries from this queue. | ||
| /// </summary> | ||
| public void Clear() | ||
| { | ||
| _queue.Clear(); | ||
| } | ||
|  | ||
| public int Count => _queue.Count; | ||
|  | ||
| /// <summary> | ||
| /// Enqueue the provided item. | ||
| /// </summary> | ||
| /// <param name="item">The item to enqueue.</param> | ||
| public void Enqueue(T item) | ||
| { | ||
| _queue.Enqueue(item); | ||
| _gate.Release(); | ||
| } | ||
|  | ||
| /// <summary> | ||
| /// Block until there is a first element in the queue and that element is ready to be dequeued then dequeue and | ||
| /// return that element. | ||
| /// </summary> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>The first element in the queue.</returns> | ||
| public async Task<T> DequeueAsync(CancellationToken cancellationToken = default) | ||
| { | ||
| while (true) | ||
| { | ||
| if (_queue.IsEmpty) | ||
| { | ||
| await _gate.WaitAsync(cancellationToken); | ||
| continue; | ||
| } | ||
| else | ||
| { | ||
| if (_queue.TryPeek(out T? item) | ||
| && _queue.TryDequeue(out T? dequeuedItem)) | ||
| { | ||
| return dequeuedItem; | ||
| } | ||
| else | ||
| { | ||
| await _gate.WaitAsync(cancellationToken); | ||
| cancellationToken.ThrowIfCancellationRequested(); | ||
| continue; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|  | ||
| /// <summary> | ||
| /// Wakeup any blocking calls not because a new element was added to the queue, but because | ||
| /// one or more elements in the queue is now ready. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Generally, this method should be called every time an item in this queue is marked as ready. | ||
| /// </remarks> | ||
| public void Signal() | ||
| { | ||
| _gate.Release(); | ||
| } | ||
|  | ||
| public void Dispose() | ||
| { | ||
| _gate.Dispose(); | ||
| } | ||
| } | ||
| } | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
      
      Oops, something went wrong.
        
    
  
      
      Oops, something went wrong.
        
    
  
  Add this suggestion to a batch that can be applied as a single commit.
  This suggestion is invalid because no changes were made to the code.
  Suggestions cannot be applied while the pull request is closed.
  Suggestions cannot be applied while viewing a subset of changes.
  Only one suggestion per line can be applied in a batch.
  Add this suggestion to a batch that can be applied as a single commit.
  Applying suggestions on deleted lines is not supported.
  You must change the existing code in this line in order to create a valid suggestion.
  Outdated suggestions cannot be applied.
  This suggestion has been applied or marked resolved.
  Suggestions cannot be applied from pending reviews.
  Suggestions cannot be applied on multi-line comments.
  Suggestions cannot be applied while the pull request is queued to merge.
  Suggestion cannot be applied right now. Please check back later.
  
    
  
    
Uh oh!
There was an error while loading. Please reload this page.