When a user takes a certain action a call (get) to a web service is made. The service doesn't allow for calls more frequent than once per second. I was thinking I could use Task.Delay to control this so that subsequent calls are made at least one second apart in time but it doesn't seem to work as expected.
The pseudo-code looks like this:
public async void OnUserAction()
{
var timeUntilPreviousCallWillBeOrWasMade = 1000ms - (Now - previousWaitStartTime);
var timeToWaitBeforeThisCallShouldBeMade = Max(0, timeUntilPreviousCallWillBeOrWasMade + 1000ms);
previousWaitStartTime = Now;
await Task.Delay(timeToWaitBeforeThisCallShouldBeMade);
MakeCallToWebService();
}
Calls and contiunations are done on same thread (as reported by Environment.CurrentManagedThreadId).
The problem is that if there's a quick succession of invocations of this method the passed time between calls to the web service is less than 1s. I've either done a silly mistake or I don't fully understand Task.Delay (or both of the above). Any hints?
It would appear that your problem happens when a request is queued up when another one is already waiting. Here's my take on your pseudo code:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ThrottledAsync
{
class Program
{
static void Main(string[] args)
{
// Queue up multiple user actions
// within a short interval.
for (var i = 0; i < 10; i++)
{
OnUserAction();
Thread.Sleep(10);
}
Console.ReadLine();
}
private static int UserActionID;
private static DateTime previousWaitStartTime;
public static async void OnUserAction()
{
// Keep track of the operation ID.
var userActionID = Interlocked.Increment(ref UserActionID);
// Pseudo-code implementation.
var timeUntilPreviousCallWillBeOrWasMade = 1000 - (int)(DateTime.Now.Subtract(previousWaitStartTime).TotalMilliseconds);
Console.WriteLine(
"{0:HH:mm:ss.ffff} - User action {1}: timeUntilPreviousCallWillBeOrWasMade = {2}.",
DateTime.Now, userActionID, timeUntilPreviousCallWillBeOrWasMade);
var timeToWaitBeforeThisCallShouldBeMade = Math.Max(0, timeUntilPreviousCallWillBeOrWasMade + 1000);
Console.WriteLine(
"{0:HH:mm:ss.ffff} - User action {1}: timeToWaitBeforeThisCallShouldBeMade = {2}.",
DateTime.Now, userActionID, timeToWaitBeforeThisCallShouldBeMade);
previousWaitStartTime = DateTime.Now;
await Task.Delay(timeToWaitBeforeThisCallShouldBeMade);
await MakeCallToWebService(userActionID);
}
private static async Task MakeCallToWebService(int userActionID)
{
// Simulate network delay.
await Task.Delay(new Random().Next(5, 10));
Console.WriteLine("{0:HH:mm:ss.ffff} - User action {1}: web service call.", DateTime.Now, userActionID);
}
}
}
And the output:
19:10:11.1366 - User action 1: timeUntilPreviousCallWillBeOrWasMade = -2147482648.
19:10:11.1416 - User action 1: timeToWaitBeforeThisCallShouldBeMade = 0.
19:10:11.1536 - User action 2: timeUntilPreviousCallWillBeOrWasMade = 988.
19:10:11.1536 - User action 2: timeToWaitBeforeThisCallShouldBeMade = 1988.
19:10:11.1586 - User action 1: web service call.
19:10:11.1646 - User action 3: timeUntilPreviousCallWillBeOrWasMade = 990.
19:10:11.1646 - User action 3: timeToWaitBeforeThisCallShouldBeMade = 1990.
19:10:11.1756 - User action 4: timeUntilPreviousCallWillBeOrWasMade = 990.
19:10:11.1756 - User action 4: timeToWaitBeforeThisCallShouldBeMade = 1990.
19:10:11.1866 - User action 5: timeUntilPreviousCallWillBeOrWasMade = 990.
19:10:11.1866 - User action 5: timeToWaitBeforeThisCallShouldBeMade = 1990.
19:10:11.1976 - User action 6: timeUntilPreviousCallWillBeOrWasMade = 990.
19:10:11.1986 - User action 6: timeToWaitBeforeThisCallShouldBeMade = 1990.
19:10:11.2086 - User action 7: timeUntilPreviousCallWillBeOrWasMade = 990.
19:10:11.2086 - User action 7: timeToWaitBeforeThisCallShouldBeMade = 1990.
19:10:11.2186 - User action 8: timeUntilPreviousCallWillBeOrWasMade = 990.
19:10:11.2196 - User action 8: timeToWaitBeforeThisCallShouldBeMade = 1990.
19:10:11.2296 - User action 9: timeUntilPreviousCallWillBeOrWasMade = 990.
19:10:11.2296 - User action 9: timeToWaitBeforeThisCallShouldBeMade = 1990.
19:10:11.2406 - User action 10: timeUntilPreviousCallWillBeOrWasMade = 990.
19:10:11.2406 - User action 10: timeToWaitBeforeThisCallShouldBeMade = 1990.
19:10:13.1567 - User action 2: web service call.
19:10:13.1717 - User action 3: web service call.
19:10:13.1877 - User action 5: web service call.
19:10:13.1877 - User action 4: web service call.
19:10:13.2107 - User action 6: web service call.
19:10:13.2187 - User action 7: web service call.
19:10:13.2187 - User action 8: web service call.
19:10:13.2357 - User action 9: web service call.
19:10:13.2537 - User action 10: web service call.
You should really be using the right tool for the job. How about a SemaphoreSlim
?
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ThrottledAsync
{
class Program
{
static void Main(string[] args)
{
// Queue up simultaneous calls.
MakeThrottledCall();
MakeThrottledCall();
MakeThrottledCall();
MakeThrottledCall();
Console.ReadLine();
}
// Used to throttle our web service calls.
// Max degree of parallelism: 1.
private static readonly SemaphoreSlim WebServiceMutex = new SemaphoreSlim(1, 1);
private static async void MakeThrottledCall()
{
// Wait for the previous call
// (and delay task) to complete.
await WebServiceMutex.WaitAsync();
try
{
await MakeCallToWebService();
// Report the completion of your web service call if necessary.
// Delay for a bit before releasing the semaphore.
await Task.Delay(1000);
}
finally
{
// Allow the next web service call to go through.
WebServiceMutex.Release();
}
}
private static async Task MakeCallToWebService()
{
// Simulate network delay.
await Task.Delay(new Random().Next(5, 10));
Console.WriteLine("WebServiceCall: {0:HH:mm:ss.ffff}", DateTime.Now);
}
}
}
EDIT: MakeThrottledCall
no longer returns Task
as per svick's comment.
Can you use Thread.Sleep(millisecondsTimeout) at msdn?
I suggest you to use that.