Search code examples
c#asynchronoustimer

How do I fix async not working correctly?


I have a script that calls api endpoints but when it does something strange is happening I can't even explain it so I'll show you the code and explain the result I am getting.

public class Program
{
    private static OpenSeaApiClient openSeaApiClient;
    private static Timer timer;
    private static int counter = 1;
    private static Collection galaxyEggs9999;


    static async Task Main()
    {
        string apiKey = "myKey";
        openSeaApiClient = new OpenSeaApiClient(apiKey);

        // Set up a timer to run the API calls every 10 seconds
        timer = new Timer(TimeSpan.FromSeconds(1).TotalMilliseconds);
        timer.Elapsed += async (sender, e) => await TimerElapsedAsync();
        timer.Start();

        Console.WriteLine("Press Enter to exit.");
        Console.ReadKey();

        // Stop the timer when exiting the program
        timer.Stop();
        timer.Dispose();
    }

    private static async Task TimerElapsedAsync()
    {
        Console.WriteLine($"Counter: {counter++}");


        if (galaxyEggs9999 == null)
        {
            galaxyEggs9999 = new Collection(openSeaApiClient, "galaxyeggs9999", "0xA08126f5E1ED91A635987071E6FF5EB2aEb67C48");
        }
        await galaxyEggs9999.ExecuteAsync();
    //more instances

    }

So TimerElapsedAsync() is set up with a Timer. When TimerElapsedAsync() starts 1 is printed and the script goes in the first instance of object. This object looks like that:

public class Collection
    {
        private readonly OpenSeaApiClient openSeaApiClient;
        private readonly string collectionName;
        private readonly string collectionContractAddress;
        public Collection(OpenSeaApiClient openSeaApiClient, string collectionName, string collectionContractAddress)
        {
            this.openSeaApiClient = openSeaApiClient;
            this.collectionName = collectionName;
            this.collectionContractAddress = collectionContractAddress;
        }
        public async Task ExecuteAsync()
        {
            //more code
                }
                else 
                {
                    var bestListingsResponse = await openSeaApiClient.GetBestListingsInCollectionResponse(CollectionName);
                    floorPriceInEtherDouble = GetCollectionFloorPriceInEther(bestListingsResponse);

                    //more code

Here comes the issue - when the script reaches

                    var bestListingsResponse = await openSeaApiClient.GetBestListingsInCollectionResponse(CollectionName);

2 is printed, after that 3,4,5 before the script continue with the other lines. And if I put a breakpoint on GetBestListingsInCollectionResponse() and press Step Over it just keeps printing incrementing numbers and not continuing.

This is what GetBestListingsInCollectionResponse() looks like if this is of any help.

 public async Task<string> GetBestListingsInCollectionResponse(string collectionSlug)
        {
            string endpoint = $"api/v2/listings/collection/{collectionSlug}/best";
            return await SendGetRequest(endpoint);
        }

I have more endpoints in my code and they all keep doing weird things like executing themselves multiple times.

Why is this happening?


Solution

  • Based on the API used you are running System.Timers.Timer, and from the Remarks section of the docs:

    The Timer component is a server-based timer that raises an Elapsed event in your application after the number of milliseconds in the Interval property has elapsed. You can configure the Timer object to raise the event just once or repeatedly using the AutoReset property.

    So it will not wait for the handler to complete and will raise the event every time the interval has elapsed.

    One workaround can be to set AutoReset to false and then enable the timer in the handler when it completes:

    timer.AutoReset = false;
    timer.Elapsed += (sender, eventArgs) =>
    {
        Thread.Sleep(1000); // simulate some work
        Console.WriteLine("elapsed"); // simulate some work
        timer.Enabled = true; // do not forget to handle the exceptions
    };
    

    But since .NET 6 you can leverage the System.Threading.PeriodicTimer which is designed for such use-cases (for example leveraging it in ASP.NET Core background workers - see here).