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

ProcessExit event handler improvements #1332

Merged
merged 7 commits into from
Apr 5, 2021
Merged

Conversation

tonyredondo
Copy link
Member

This PR improves the way we handle the ProcessExit event by ensuring that the tracer handler is the last one to be executed from the event.

The background:

The process signals received on the CLR are handled this way:

  1. SIGTERM is handled by AppDomain.CurrentDomain.ProcessExit
  2. SIGINT and SIGQUIT is handled by Console.CancelKeyPress
  3. SIGKILL can't be caught or ignored.

#1323 (comment)

In scenarios where IHostApplicationLifetime is being used we can face the case where the tracer handles the ProcessExit event where the traces are flushed and the writer is closed but the actual application can still be creating new spans. As @andrewlock describes in the discussion here: #1323 (comment)

This PR solves the issue by ensuring the tracer handler for ProcessExit will be the last one of the event, to send all the spans generated on previous handlers.

Also on SIGINT and SIGQUIT signals changes the actual behavior to only flush and not close due we can receive a SIGTERM later.

@DataDog/apm-dotnet

@tonyredondo tonyredondo self-assigned this Apr 1, 2021
@tonyredondo tonyredondo requested a review from a team as a code owner April 1, 2021 23:34
@tonyredondo tonyredondo changed the title Improves the ProcessExit event handler ProcessExit event handler improvements Apr 1, 2021
@tonyredondo tonyredondo marked this pull request as draft April 1, 2021 23:45
@tonyredondo tonyredondo marked this pull request as ready for review April 2, 2021 00:01
Copy link
Member

@andrewlock andrewlock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! 😄 Witchcraft! 🧙

Copy link
Contributor

@zacharycmontoya zacharycmontoya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seriously, some pure wizardry right here!

@lucaspimentel
Copy link
Member

I see a lot of comments about SIGTERM, SIGINT, SIGQUIT, and SIGKILL. Was any of this tested on Windows (.NET Core and .NET Framework)?

@tonyredondo
Copy link
Member Author

tonyredondo commented Apr 2, 2021

I see a lot of comments about SIGTERM, SIGINT, SIGQUIT, and SIGKILL. Was any of this tested on Windows (.NET Core and .NET Framework)?

The principal delegates functionality is being tested with the unit tests in all platforms and framework versions, including arm64.

About both events CTRL+C and ProcessExit. The behavior is pretty similar, the only difference is now we don’t close the agent writer on CTRL+C. Because we were assuming that key combination always finish the process and that’s not true as seen in the example with the dd-trace tool and the .Cancel property in the event args.

However I’ll run a simple console app test with a tracer instance before merge to be 100% sure.

For the edge case we are solving here, @andrewlock ran a basic test using a prototype of this solution before the PR creation. The console app test will include this scenario using the tracer.

@tonyredondo
Copy link
Member Author

I ended up using the same logic for CancelKeyPress event as well... to handle all the cases...

Scenario 1 and 2:

Code:

        static async Task Main(string[] args)
        {
            _ = Tracer.Instance;
            Console.WriteLine("Hello World!");
            await Task.Delay(5000).ConfigureAwait(false);
        }
  1. Wait for normal exit (ProcessExit handler):
Hello World!
  FlushAndCloseAsync
  1. Ctrl+C before finishing the Task.Delay (CancelKeyPress handler / ProcessExit didn't trigger):
Hello World!
  FlushAndCloseAsync
^C

Scenario 3 and 4:

Code:

        static async Task Main(string[] args)
        {
            _ = Tracer.Instance;
            AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
            Console.WriteLine("Hello World!");
            await Task.Delay(5000).ConfigureAwait(false);
        }

        private static void CurrentDomain_ProcessExit(object sender, EventArgs e)
        {
            Console.WriteLine("Finishing the application...");
        }
  1. Wait for normal exit (ProcessExit handler):
Hello World!
Finishing the application...
  FlushAndCloseAsync
  1. Ctrl+C before finishing the Task.Delay (CancelKeyPress handler / ProcessExit didn't trigger):
Hello World!
  FlushAndCloseAsync
^C

Scenario 5 and 6:

Code:

        static async Task Main(string[] args)
        {
            _ = Tracer.Instance;
            Console.CancelKeyPress += Console_CancelKeyPress;
            Console.WriteLine("Hello World!");
            await Task.Delay(5000).ConfigureAwait(false);
        }

        private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
        {
            Console.WriteLine($"Cancel Key Pressed: {e.Cancel}");
            e.Cancel = true; // Don't exit the process
        }
  1. Wait for normal exit (ProcessExit handler):
Hello World!
  FlushAndCloseAsync
  1. Ctrl+C before finishing the Task.Delay [CancelKeyPress handler (FlushTracesAsync) and ProcessExit handler (FlushAndCloseAsync)]:
Hello World!
Cancel Key Pressed: False
  FlushTracesAsync
  FlushAndCloseAsync

Scenario 7 and 8:

Code:

        static async Task Main(string[] args)
        {
            _ = Tracer.Instance;
            Console.CancelKeyPress += Console_CancelKeyPress;
            Console.WriteLine("Hello World!");
            await Task.Delay(5000).ConfigureAwait(false);
        }

        private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
        {
            Console.WriteLine($"Cancel Key Pressed: {e.Cancel}");
            e.Cancel = false; // Exit the process
        }
  1. Wait for normal exit (ProcessExit handler):
Hello World!
  FlushAndCloseAsync
  1. Ctrl+C before finishing the Task.Delay [CancelKeyPress handler (FlushAndCloseAsync)]:
Hello World!
Cancel Key Pressed: False
  FlushAndCloseAsync
^C

Scenario 9 and 10:

Code:

    class Program
    {
        static async Task Main(string[] args)
        {
            _ = Tracer.Instance;
            Console.CancelKeyPress += Console_CancelKeyPress;
            AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
            Console.WriteLine("Hello World!");
            await Task.Delay(5000).ConfigureAwait(false);
        }

        private static void CurrentDomain_ProcessExit(object sender, EventArgs e)
        {
            Console.WriteLine("Finishing the application...");
        }

        private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
        {
            Console.WriteLine($"Cancel Key Pressed: {e.Cancel}");
            e.Cancel = true; // Don't exit the process
        }
    }
  1. Wait for normal exit (ProcessExit handler):
Hello World!
Finishing the application...
  FlushAndCloseAsync
  1. Ctrl+C before finishing the Task.Delay [CancelKeyPress handler (FlushTracesAsync) and ProcessExit handler (FlushAndCloseAsync)]:
Hello World!
Cancel Key Pressed: False
  FlushTracesAsync
Finishing the application...
  FlushAndCloseAsync

Scenario 11 and 12:

Code:

    class Program
    {
        static async Task Main(string[] args)
        {
            _ = Tracer.Instance;
            Console.CancelKeyPress += Console_CancelKeyPress;
            AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
            Console.WriteLine("Hello World!");
            await Task.Delay(5000).ConfigureAwait(false);
        }

        private static void CurrentDomain_ProcessExit(object sender, EventArgs e)
        {
            Console.WriteLine("Finishing the application...");
        }

        private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
        {
            Console.WriteLine($"Cancel Key Pressed: {e.Cancel}");
            e.Cancel = false; // Exit the process
        }
    }
  1. Wait for normal exit (ProcessExit handler):
Hello World!
Finishing the application...
  FlushAndCloseAsync
  1. Ctrl+C before finishing the Task.Delay [CancelKeyPress handler (FlushTracesAsync) and ProcessExit handler (FlushAndCloseAsync)]:
Hello World!
Cancel Key Pressed: False
  FlushAndCloseAsync
^C

@tonyredondo
Copy link
Member Author

tonyredondo commented Apr 5, 2021

For IHostApplicationLifetime I've tested it with a simple Worker app:

    public class Program
    {
        public static void Main(string[] args)
        {
            _ = Tracer.Instance;
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<Worker>();
                });
    }

    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;

        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }

        public override Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("StartAsync");
            return base.StartAsync(cancellationToken);
        }

        public override Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("StopAsync");
            return base.StopAsync(cancellationToken);
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                await Task.Delay(1000, stoppingToken);
            }
        }
    }

Output after pressing CTRL+C:

info: WorkerService1.Worker[0]
      StartAsync
info: WorkerService1.Worker[0]
      Worker running at: 04/05/2021 14:56:03 +02:00
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Users\danielredondo\source\repos\WorkerService1\WorkerService1
info: WorkerService1.Worker[0]
      Worker running at: 04/05/2021 14:56:04 +02:00
info: WorkerService1.Worker[0]
      Worker running at: 04/05/2021 14:56:05 +02:00
info: WorkerService1.Worker[0]
      Worker running at: 04/05/2021 14:56:06 +02:00
  FlushTracesAsync
info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
info: WorkerService1.Worker[0]
      StopAsync
  FlushAndCloseAsync

@lucaspimentel lucaspimentel added the area:tracer The core tracer library (Datadog.Trace, does not include OpenTracing, native code, or integrations) label Apr 5, 2021
@tonyredondo tonyredondo force-pushed the tony/process-exit-event branch from 2aa6cad to 71e653d Compare April 5, 2021 13:48
@tonyredondo tonyredondo merged commit 0aaa30e into master Apr 5, 2021
@tonyredondo tonyredondo deleted the tony/process-exit-event branch April 5, 2021 18:26
tonyredondo added a commit that referenced this pull request Apr 16, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:tracer The core tracer library (Datadog.Trace, does not include OpenTracing, native code, or integrations)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants