4

Actively polling an endpoint in .NET 6

 1 year ago
source link: https://maciejz.dev/actively-polling-http-endpoint/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Article

Actively polling an endpoint in .NET 6

Actively polling an endpoint in .NET 6

I was recently working on a document e-signing solution that had me integrating with PandaDoc. The whole process was very straightforward, but what I liked the most was that I found out about a new .NET API that I had not seen before!

The two main steps in the process were uploading a PDF with placeholders (called Field Tags) to PandaDoc and then sharing the document with a customer (with the placeholders being converted into fillable form fields by then). Seems simple, and it was, but take a look at the documentation:

image-2.png

Between the two main points I mentioned above, there is another little step: "Wait for the Document to enter draft status". It's not surprising, really - it would actually be surprising if the document was ready the moment you upload it - what would I be paying for, then? 😁There are two options here - you can either:

  • set up a webhook listener, and the document status changes will be sent to you
  • actively poll the document status endpoint until the desired status is reached

I would usually prefer the first option, but in my case, it did not make much sense to go that way without a major rewrite of a few processes, so I decided to go with active polling.

In my proof-of-concept implementation, I used Task.Delay to introduce some delay between the requests to avoid being blocked by PandaDoc.

var documentCreationOptions = new DocumentCreationOptions(...);
var (documentId, documentStatus) = await _pandaDocClient.CreateDocument(file, fileName, documentCreationOptions);
while (status != "document.draft")
{
    await Task.Delay(PollingInterval);
    documentStatus = await _pandaDocClient.GetDocumentStatus(documentId);
    if (status != "document.uploaded" && status != "document.draft")
    {
        throw new InvalidDocumentStatusException($"Invalid document status. DocumentId: {documentId}, Status: {documentStatus}.")
    }
}
await _pandaDocClient.SendDocument(documentId);
123456789101112

It worked, but when I was cleaning up the solution and moving it to the main project, I said to myself: there should be a nicer way to do this - it's 2022, after all.

A quick Google search revealed what I was looking for:

profile.png

The project was just migrated to .NET 6 so I got lucky, the cleaned-up version is not very different, but I like it much more:

var documentCreationOptions = new DocumentCreationOptions(...);
var (documentId, documentStatus) = await _pandaDocClient.CreateDocument(file, fileName, documentCreationOptions);
using (var timer = new PeriodicTimer(PollingInterval))
{
    while (status != "document.draft" && await timer.WaitForNextTickAsync())
    {
        documentStatus = await _pandaDocClient.GetDocumentStatus(documentId);
        if (status != "document.uploaded" && status != "document.draft")
        {
            throw new InvalidDocumentStatusException($"Invalid document status. DocumentId: {documentId}, Status: {documentStatus}.")
        }
    }
}
await _pandaDocClient.SendDocument(documentId);
1234567891011121314

So what's to like here? At first glance, all it did was introduce another nesting level for the using statement. Well, that one can be easily fixed by moving the waiting part to a separate method and converting the using statement to a using declaration.

So under the hood, both approaches use the TimerQueueTimer class. The difference is that the new PeriodicTimer allocated just one TimerQueueTimer instance while each Task.Delay() creates a new object that later needs to be cleaned up too. This might not seem like a huge gain - especially here, since the wait time for the document state change should not be too long. But in a large system, those allocations might pile up, so it's always nice to make less of them when possible.

The PeriodicTimer is not a silver bullet and in some cases, Task.Delay will be the better option. With the new timer, the time between one operation finishing and the next one starting will be at most what you set as the interval. With Task.Delay, the time will be more or less what you choose. Here's a little diagram to help visualize it:

image-15.png

So, as usual, use the tool that makes the most sense in your situation.

I know that some people will come and say that I could've used the Timer class and be done with it. My first answer would be - which one? On a more serious note, the one thing that disqualified Timer for me in this scenario is that Timer callbacks can overlap, which does not make much sense when polling an external endpoint.

If you want more insight into how PeriodicTimer came to be, head over to the GitHub issue where it all happened:

31525

Cover photo by Renel Wackett on Unsplash


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK