Search code examples
c#.net-coretaskbackgroundworkerkubernetes-cronjob

How do I stop a Worker service container after it has completed?


I have a .Net Worker service that runs as a K8S Cronjob but when it runs to completion the service is not exiting as expected. The pod remains in running state and thus the K8S job never completes as in the logs below :

[40m[1m[33mwarn[39m[22m[49m: UploaderCron.Worker[0]
  Worker STARTING
[40m[32minfo[39m[22m[49m: UploaderCron.Worker[0]
      Worker running at: 11/04/2022 11:27:01 +00:00
Connected to SFTP Server
/taat/DEV-AAA-20221104T112701/DatabaseA20221104112606.gz
File uploaded successfully
/taat/DEV-BBB-20221104T112701/DatabaseB20221104112606.gz
File uploaded successfully
....
....
Backup file successfully purged
Backup file successfully purged
[40m[32minfo[39m[22m[49m: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
[40m[32minfo[39m[22m[49m: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
[40m[32minfo[39m[22m[49m: Microsoft.Hosting.Lifetime[0]
      Content root path: /app
 [40m[32minfo[39m[22m[49m: UploaderCron.Worker[0]
      Worker running at: 11/04/2022 11:35:46 +00:00
Connected to SFTP Server
[40m[32minfo[39m[22m[49m: UploaderCron.Worker[0]
      Worker running at: 11/04/2022 11:44:08 +00:00
Connected to SFTP Server
[40m[32minfo[39m[22m[49m: UploaderCron.Worker[0]
      Worker running at: 11/04/2022 11:52:31 +00:00
Connected to SFTP Server

EXPECTED

  1. The worker service should run daily (to completion) at a specific time that is specified in the K8S manifest.
  2. The worker service should connect only once to the SFTP server and execute all required operations (uploads) to completion. By execute I mean the upload directory should be created once and only once.
  3. The container should stop once the task has been completed.
  4. After the container stops the corresponding pod/cronjob should change to Completed status.

ACTUAL

  1. The worker service runs to completion i.e creates the upload directory as well as upload all associated files
  2. The worker service is running again after lapse of the time specified in
    await Task.Delay(450000, stoppingToken);
    

In each of these cases the service is connecting again to the sftp server and creating the upload directory (no files are being uploaded this time, only the directory is being created).

  1. The worker service pod remains in running status and the container logs show that this line is never being invoked :
_logger.LogWarning("Worker STOPPING: {time}", DateTimeOffset.Now);

This is my Worker code :

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
    _logger = logger;
}

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

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

    public override Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogWarning("Worker STOPPING: {time}", DateTimeOffset.Now);
        return base.StopAsync(cancellationToken);
    }
}

Helper.cs

public static class Helper
{
   public static async Task UploadFiles()
   {  
      using (var fileStream = System.IO.File.OpenRead(localFilePath))
                {
                   sftp.UploadFile(fileStream, remoteFilepath);
                    Console.WriteLine("File uploaded successfully");               
                 }               
    //more upload logic 
   }
 public static async Task LoadFiles()
        {   
               await Helper.UploadFiles();   
        }
}

The K8S CronJob for the worker service (container uploader) :

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: dbbackup
spec:
  schedule: "26 11 * * *" #Cron job everyday at 11:26am
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          affinity:
            podAffinity:
              requiredDuringSchedulingIgnoredDuringExecution:
                - labelSelector: 
                    matchLabels:
                      app.kubernetes.io/name: mongodb
                  topologyKey: "kubernetes.io/hostname"
          initContainers:
            - name: backup
              image: backup:latest
              imagePullPolicy: "IfNotPresent"
              volumeMounts:
                - mountPath: /tmp
                  name:  datadir
              command: ['sh', '-c',"./dump.sh"]
          containers:
            - name: uploader
              image: uploader:latest
              imagePullPolicy: "IfNotPresent"
              volumeMounts:
                - mountPath: /tmp
                  name:  datadir
          restartPolicy: OnFailure
          volumes:
            - name: datadir
              persistentVolumeClaim:
                claimName: mongodb-taat

NB:For context I am following the reference implementation given here

What am I missing so that the container stops once the task is completed ?


Solution

  • Well, right here your background service is deliberately running until it is requested to stop, periodically doing its work:

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

    If you want the application to exit when it completes, then just do that:

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
      try
      {
        _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
        await Helper.LoadFiles();
      }
      finally
      {
        _hostApplicationLifetime.StopApplication();
      }
    }
    private readonly IHostApplicationLifetime _hostApplicationLifetime;
    public Worker(ILogger<Worker> logger, IHostApplicationLifetime hostApplicationLifetime)
    {
      _logger = logger;
      _hostApplicationLifetime = hostApplicationLifetime;
    }