# Taustapalvelut ASP.NET Core - Osa 1: Lähestymistavat

<!--category-- ASP.NET Core, IHostedService, BackgroundService, Hangfire -->
<datetime class="hidden">2025-11-27T09:00</datetime>

Jokaisessa nykyaikaisessa verkkosovelluksessa on töitä, joiden ei pitäisi estää HTTP-pyyntöä – sähköpostien lähettämistä, tiedostojen käsittelyä, synkronointia ulkoisten palveluiden kanssa, määräaikaishuoltoa. ASP.NET Core tarjoaa useita lähestymistapoja tämän taustatyön käsittelyyn yksinkertaisesta `IHostedService` Hangfiren kaltaisten pitkälle kehitettyjen kehysten toteutukset. Tässä ensimmäisessä osassa tutkitaan peruskuvioita ja niiden käyttöä.

# Johdanto

Tunnustus; Tykkään Taustapalveluista LOT, tällä sivustolla (BLOG-sivusto!) on yli puoli tusinaa niitä tekemässä karuja taustatehtäviä, mutta kuten kaikki heillä on joitakin ODDIOT ja käytäntöjä, jotka tekevät niiden käytöstä paljon miellyttävämpää.
Taustapalvelut ovat nykyaikaisten verkkosovellusten unsung sankareita. Vaikka ohjaimesi käsittelevät HTTP-pyyntöjä etualalla, taustapalveluissa käsitellään hiljaa jonotettuja sähköposteja, hakujen indeksisisältöä, ulkoisia sovellusliittymiä, tilapäisten tiedostojen siivoamista ja lukemattomia muita tehtäviä, jotka muuten tukkisivat pyyntöputkesi.

Tässä kaksiosaisessa sarjassa tutustumme ASP.NET Coren taustapalveluiden toteuttamiseen eri tavoin kuin sisäänrakennettu ASP.NET Core `IHostedService` sekä `BackgroundService` Hangfiren kaltaisiin kehittyneempiin ratkaisuihin liittyviä abstrakteja. Osassa 1 tarkastellaan perustavanlaatuisia lähestymistapoja ja niiden ominaisuuksia. [2 osa](/blog/background-services-in-aspnetcore-part2)Sukellamme tosimaailman toteutuksiin tuotantokoodipohjalta.

> **Tärkeää:** Kiinnitämme erityistä huomiota elinkaaren hallintaan, erityisesti usein ylikatsottuihin `StopAsync` menetelmä, jossa monet kehittäjät kohtaavat kryptisiä poikkeuksia, kun niiden sovellukset sulkeutuvat.

[TOC]

# Miksi taustapalvelut?

Ennen kuin sukellat "miten", mietitään lyhyesti "miksi". Taustapalveluiden avulla voit:

1. **Tyhjennä hitaat toiminnot** - Älä anna käyttäjien odottaa, kun lähetät sähköposteja tai luot PDF-tiedostoja
2. **Aikataulu toistuvia tehtäviä** - Siivoa vanhat levyt joka ilta kello 2.
3. **Prosessijonot** - Käsittele kanavien tai viestinvälittäjien viestejä
4. **Ulkoisen tilan seuranta** - Kyselyrajapinnat tai seurantatiedostojärjestelmät muutoksia varten
5. **Koordinoi monimutkaisia työnkulkuja** - Hallitse monivaiheisia prosesseja, jotka kestävät minuutteja tai tunteja

ASP.NET Core tarjoaa useita lähestymistapoja näiden palvelujen toteuttamiseen, joissa kaikissa on erilaisia myönnytyksiä.

# Historiallinen tausta: Miksi taustapalvelut ovat nyt toteutettavissa

"Vanhoina päivinä" (ennen vuotta 2010) verkkosovelluksen taustatyötä pidettiin yleisesti huonona ideana. Perinteinen viisaus oli: "Verkkopalvelimet käsittelevät verkkopyyntöjä. Taustatyö kuuluu erilliselle palvelimelle."

Tämä ei ollut vain rahtikulttiviisautta, vaan se perustui todellisiin teknisiin rajoituksiin:

## Yhden luukun aikakausi

Varhaiset verkkopalvelimet (ja rehellisesti sanottuna nyt "halvat" Azure-palvelut) toimivat tyypillisesti **Yksi- tai kaksiydinsuorittimet**. Jos suoritit CPU-intensiivisen taustatehtävän, se kilpaili suoraan saman ytimen verkkopyyntöjen kanssa:

```
Single Core (2005):
┌─────────────────────┐
│  Background Task    │  ← Uses 80% CPU
│  (80% of core)      │
├─────────────────────┤
│  Web Requests       │  ← Only 20% left!
│  (20% of core)      │  ← Slow responses
└─────────────────────┘
```

Tulos: Sivustostasi tuli verkkainen, kun taustatyö alkoi.

## Kierrealtaan nälänhätä

Käytetty Classic ASP.NET **langan per-pyyntö**. Verkkopooli oli suhteellisen pieni (tyypillisesti 25-100 lankaa), ja taustatehtävät varastaisivat verkkopyyntöjä käsitteleviä lankoja:

```csharp
// Classic ASP.NET (2008)
ThreadPool.QueueUserWorkItem(_ =>
{
    // This steals a thread from the pool!
    ProcessLongRunningTask();
});

// Meanwhile, web requests are queued waiting for threads
// HTTP 503 Service Unavailable
```

## IIS:n sovelluspoolin kierrätys

IIS kierrättäisi aggressiivisesti sovelluspooleja (aloita sovellus uudelleen) muistirajoihin, pyyntömääriin tai aikatauluihin perustuen. Taustatyö lopetettaisiin kesken toiminnan:

```
00:00 - Background import starts (2 hour task)
02:00 - IIS recycles app pool (scheduled)
      - Background task killed
      - Work lost, must start again
```

## Rajoitettu Asunc/Await-tuki

Ennen .NET-verkkoa 4.5 (2012) asynkkiohjelmointi oli (suhteellisen) tuskallista. Taustatehtävät tukkivat usein kierteet tarpeettomasti:

```csharp
// Pre-async (2008)
void ProcessEmails()
{
    foreach (var email in GetEmails())
    {
        smtp.Send(email);  // Blocks thread for 500ms per email
    }
}
// 100 emails = 50 seconds of blocked thread time
```

## Mikä muuttui: nykyinen aikakausi

Nykypäivän maisema on dramaattisesti erilainen:

### 1. Multi-Core on edullinen

Ekonomiikka on kääntynyt nousuun. Cloud VM:t, joissa on useita ytimiä, ovat kohtuullisesti hinnoiteltuja, ja paljaat metallipalvelimet ovat yllättävän halpoja. Tämä blogi on omistettu 8-ydinpalvelimelle, joka maksaa vähemmän kuin vastaava Azure VM – ja saan kaikki nuo ytimet itselleni, ei meluisia naapureita. Yhden ytimen taustatehtävä ei vaikuta merkittävästi verkkopyyntöihin muissa ytimissä:

```
8-Core Server (2024):
Core 1: ████████████████████ Web Requests
Core 2: ████████████████████ Web Requests
Core 3: ████████████████████ Web Requests
Core 4: ████████████████████ Web Requests
Core 5: ████████████████████ Background Task ← Isolated
Core 6: ████████████████████ Background Task
Core 7: ████████████████████ Background Task
Core 8: ████████████████████ Background Task
```

### 2. Async/Aodota kaikkialla

Modern .NET tekee async-ohjelmoinnista triviaalia. Taustatehtävät voivat odottaa I/O:ta estämättä kierteitä:

```csharp
// Modern async (2024)
async Task ProcessEmailsAsync(CancellationToken ct)
{
    await foreach (var email in GetEmailsAsync(ct))
    {
        await smtp.SendAsync(email, ct);  // Doesn't block thread!
    }
}
// 100 emails processed efficiently, thread returns to pool during I/O
```

### 3. Paremman prosessin isännöinti

- [**Docker**](https://www.docker.com/) - Konttien taustapalvelut eivät kierräty mielivaltaisesti
- [**Kubernetit**](https://kubernetes.io/) - Sopiva sammutuskäsittely `SIGTERM`
- [**järjestelmällinen**](https://systemd.io/) - Linux-palvelut, jotka käynnistyvät luotettavasti
- **Windows-palvelut** - Oikea pitkäkestoinen prosessimalli

### 4. Kanavat ja modernit alkuaineet

.NET-verkolla on nyt ensimmäisen luokan tuki samanaikaiselle ohjelmoinnille [`System.Threading.Channels`](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels):

```csharp
// System.Threading.Channels
var channel = Channel.CreateBounded<Email>(100);

// Producer (web request)
await channel.Writer.WriteAsync(email);  // Fast, non-blocking

// Consumer (background service)
await foreach (var email in channel.Reader.ReadAllAsync())
{
    await ProcessAsync(email);  // Efficient, async
}
```

### 5. Resurssirajat ja ryhmät

Nykyaikaiset konttiorkesterit sallivat sinun **rajatkaa resurssien käyttö**:

```yaml
# Kubernetes resource limits
resources:
  limits:
    cpu: "500m"        # Background task can't use more than 0.5 CPU
    memory: "512Mi"    # Or more than 512 MB RAM
```

Tämä tarkoittaa, että karannut taustatehtävä ei voi näännyttää verkkotasoasi nälkään.

Kysymys ei ole enää "Voimmeko ajaa taustapalveluita verkkosovelluksessamme?" vaan "**Pitäisikö meidän?**"Tutkimme tätä päätöstä "Kun et käytä taustapalveluita" -osiossa myöhemmin.

# Sisäänrakennetut vaihtoehdot

## IHostedService: The Foundation

ASP.NET Coren jokainen taustapalvelu toimii ytimessään [`IHostedService`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice). Tämä rajapinta on kauniisti yksinkertainen:

```csharp
public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}
```

Siinä kaikki, kaksi keinoa. `StartAsync` soitetaan, kun hakemuksesi alkaa, ja `StopAsync` Kun se sulkeutuu.

Rekisteröi palvelusi `Program.cs`:

```csharp
builder.Services.AddHostedService<MyBackgroundService>();
```

Tässä elinkaari visualisoituna:

```mermaid
graph LR
    A[Application Starts] --> B[StartAsync Called]
    B --> C[Service Running]
    C --> D[Application Shutting Down]
    D --> E[StopAsync Called]
    E --> F[Application Stopped]

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style C stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style F stroke:#dc2626,stroke-width:3px,color:#ef4444
```

### StartAsync: Synkroninen vs. Asynkroninen aloitus

Kriittinen päätös täytäntöönpanossa `IHostedService` on se, onko `StartAsync` menetelmän pitäisi estää tai palata välittömästi.

**Synkroninen (esto) käynnistys:**

```csharp
public class BlockingStartService : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // This blocks application startup until complete
        await InitializeDatabaseAsync(cancellationToken);
        await LoadConfigurationAsync(cancellationToken);

        // Only now will the application continue starting
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;
}
```

**Asynkroninen (ei-esto) alku:**

```csharp
public class NonBlockingStartService : IHostedService
{
    private Task _backgroundTask;
    private readonly CancellationTokenSource _cts = new();

    public Task StartAsync(CancellationToken cancellationToken)
    {
        // Start background work but return immediately
        _backgroundTask = Task.Run(async () =>
        {
            // Give other services time to initialise
            await Task.Delay(TimeSpan.FromSeconds(5), _cts.Token);
            await DoLongRunningWorkAsync(_cts.Token);
        }, _cts.Token);

        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _cts.Cancel();
        await _backgroundTask; // Wait for completion
    }
}
```

**Kunkin lähestymistavan käyttö:**

- **Esto:** Kun palvelun on saatava alustaminen valmiiksi ennen kuin sovellus voi käsitellä pyyntöjä (esim. kriittisen kokoonpanon lataaminen, välimuistien lämpeneminen)
- **Muu kuin esto:** Kun palvelu voi alustautua taustalla muiden palveluiden aloittaessa (esim. olemassa olevan sisällön indeksointi, synkronointi ulkoisten palveluiden kanssa)

### StopAsync: Yhteinen ansa

Tässä kohtaa tilanne muuttuu mielenkiintoiseksi – ja monet kehittäjät kohtaavat ongelmia. Kun sovellus sammuu, ASP.NET Core soittaa `StopAsync` Kaikissa isäntäpalveluissa. Sinulla on rajoitettu aika (oletus 5 sekuntia) siivota sulavasti. `Program.cs`:

```csharp
builder.Services.Configure<HostOptions>(options =>
{
    options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
```

**Yleisin virhe:**

```csharp
public class BrokenService : IHostedService
{
    private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
    private Task _processingTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _processingTask = ProcessMessagesAsync();
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // WRONG: The channel is still open, ProcessMessagesAsync
        // will hang on WaitToReadAsync forever!
        return Task.CompletedTask;
    }

    private async Task ProcessMessagesAsync()
    {
        // This will never exit because the channel is never completed
        await foreach (var message in _channel.Reader.ReadAllAsync())
        {
            await ProcessAsync(message);
        }
    }
}
```

Kun teet tämän palvelun ja lopetat sovelluksen, näet virheitä, kuten:

```
Unable to cast object of type 'TaskCompletionSource`1[System.Threading.Tasks.VoidTaskResult]' to type 'System.Threading.Tasks.Task'
```

Tai sovellus yksinkertaisesti roikkuu sulkuajan ajan ennen kuin se loppuu voimallisesti.

**Oikea lähestymistapa:**

```csharp
public class CorrectService : IHostedService
{
    private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
    private readonly CancellationTokenSource _cts = new();
    private Task _processingTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _processingTask = ProcessMessagesAsync(_cts.Token);
        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        // CORRECT: Signal cancellation and complete the channel
        await _cts.CancelAsync();
        _channel.Writer.Complete();

        try
        {
            // Wait for processing to finish or for the shutdown timeout
            await Task.WhenAny(_processingTask,
                Task.Delay(Timeout.Infinite, cancellationToken));
        }
        catch (OperationCanceledException)
        {
            // Expected when shutdown timeout is reached
        }
    }

    private async Task ProcessMessagesAsync(CancellationToken token)
    {
        await foreach (var message in _channel.Reader.ReadAllAsync(token))
        {
            try
            {
                await ProcessAsync(message);
            }
            catch (OperationCanceledException)
            {
                // Shutdown requested, exit gracefully
                break;
            }
        }
    }
}
```

**StopAsyncin oikean toteutuksen avainkohdat:**

1. **Signaalin peruminen** - Käytä a: a `CancellationTokenSource` ja perua se
2. **Täydelliset kanavat** - Jos käytät kanavia, soita. `Writer.Complete()`
3. **Odota taustatehtäviä** - Käytä `Task.WhenAny` Shutdown-peruuttamiskyltillä
4. **Hoitakaa operaatioCancedExclusive** - Tätä odotetaan ja se pitäisi saada kiinni
5. **älä heitä poikkeuksia** - Poikkeukset `StopAsync` voi aiheuttaa arvaamatonta käytöstä

## Taustapalvelu: Kätevä peruskurssi

Kirjoitetaan `IHostedService` Toteutukset voivat olla toistuvia. Tarvitset aina taustatehtävän, peruutusviestin lähteen ja saman siivouskuvion. [`BackgroundService`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice) Hoitaa tämän kattilan puolestasi:

```csharp
public abstract class BackgroundService : IHostedService, IDisposable
{
    private Task _executeTask;
    private CancellationTokenSource _stoppingCts;

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        _executeTask = ExecuteAsync(_stoppingCts.Token);
        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        if (_executeTask == null) return;

        try
        {
            _stoppingCts.Cancel();
        }
        finally
        {
            await Task.WhenAny(_executeTask, Task.Delay(Timeout.Infinite, cancellationToken));
        }
    }

    public virtual void Dispose()
    {
        _stoppingCts?.Cancel();
    }
}
```

Toteutat vain `ExecuteAsync` ja anna perusluokan hoitaa putkistot:

```csharp
public class SimpleBackgroundService : BackgroundService
{
    private readonly ILogger<SimpleBackgroundService> _logger;

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

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Service starting");

        // Wait for app to finish starting
        await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await DoWorkAsync(stoppingToken);
                await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
            }
            catch (OperationCanceledException)
            {
                // Shutdown requested
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in background service");
            }
        }

        _logger.LogInformation("Service stopping");
    }

    private async Task DoWorkAsync(CancellationToken token)
    {
        _logger.LogInformation("Doing work...");
        // Your actual work here
        await Task.Delay(1000, token);
    }
}
```

### Milloin voit käyttää taustapalvelua vastaan iHostedService

**Käyttö `BackgroundService` kun:**

- Tarvitset pitkäaikaisen taustasilmukan
- Haluat yksinkertaisen jaksottaisen suorituksen
- Et tarvitse hyvää kontrollia. `StartAsync`/`StopAsync` ajoitus

**Käyttö `IHostedService` kun:**

- Sinun täytyy kontrolloida tarkkaan, mitä tapahtuu `StartAsync` vs. taustatyö
- Asetat tapahtumakäsittelijöitä tai katselijoita jatkuvan silmukan sijaan
- Sinun täytyy koordinoida muiden palveluiden kanssa käynnistyksen aikana

# Advanced: Startup-koordinointi

Joskus tarvitaan palveluita, jotta voidaan odottaa toisiaan. Esimerkiksi semanttinen hakuindeksoija saattaa haluta odottaa, kunnes maaliinladatun tiedoston prosessori on saanut alkulatauksensa valmiiksi.

Tässä on malli palvelun käynnistyksen koordinoimiseksi:

```csharp
public interface IStartupCoordinator
{
    void RegisterService(string serviceName);
    void SignalReady(string serviceName);
    bool IsServiceReady(string serviceName);
    Task WaitForServiceAsync(string serviceName, CancellationToken cancellationToken = default);
    Task WaitForAllServicesAsync(CancellationToken cancellationToken = default);
}

public class StartupCoordinator : IStartupCoordinator
{
    private readonly ConcurrentDictionary<string, TaskCompletionSource> _services = new();
    private readonly ILogger<StartupCoordinator> _logger;

    public void RegisterService(string serviceName)
    {
        _services.TryAdd(serviceName, new TaskCompletionSource());
    }

    public void SignalReady(string serviceName)
    {
        if (_services.TryGetValue(serviceName, out var tcs))
        {
            tcs.TrySetResult();
            _logger.LogInformation("{Service} is ready", serviceName);
        }
    }

    public async Task WaitForServiceAsync(string serviceName, CancellationToken ct = default)
    {
        if (_services.TryGetValue(serviceName, out var tcs))
        {
            await tcs.Task.WaitAsync(ct);
        }
    }

    public async Task WaitForAllServicesAsync(CancellationToken ct = default)
    {
        await Task.WhenAll(_services.Values.Select(tcs => tcs.Task)).WaitAsync(ct);
    }
}
```

Käyttö palvelussa:

```csharp
public class DependentService : IHostedService
{
    private readonly IStartupCoordinator _coordinator;
    private readonly ILogger<DependentService> _logger;

    public DependentService(
        IStartupCoordinator coordinator,
        ILogger<DependentService> logger)
    {
        _coordinator = coordinator;
        _logger = logger;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // Wait for another service to be ready
        await _coordinator.WaitForServiceAsync("MarkdownProcessor", cancellationToken);

        _logger.LogInformation("Dependencies ready, starting work");

        // Do your work...

        // Signal you're ready for services that depend on you
        _coordinator.SignalReady("DependentService");
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;
}
```

Tästä kuviosta on hyötyä erityisesti silloin, kun on useita taustapalveluita, joissa on riippuvuussuhteita.

# Jaettu koordinointi Redisin kanssa

Startup-koordinaattori toimii yhden sovellusinstallaation sisällä. Mutta mitä tapahtuu, kun skaalaudut useaan kertaan? Et halua, että kaikki kolme tapahtumaa hoitavat saman aikataulun mukaisen tehtävän samanaikaisesti.

[Redis](https://redis.io/) Tarjoaa yksinkertaisen ratkaisun: käytä lippuja (avaimia) koordinoidaksesi, kuka tekee mitäkin.

## Yksinkertaiset johtajavaalit

```csharp
public class DistributedBackgroundService : BackgroundService
{
    private readonly IConnectionMultiplexer _redis;
    private readonly ILogger<DistributedBackgroundService> _logger;
    private readonly string _instanceId = Guid.NewGuid().ToString();
    private const string LeaderKey = "background:newsletter:leader";

    public DistributedBackgroundService(
        IConnectionMultiplexer redis,
        ILogger<DistributedBackgroundService> logger)
    {
        _redis = redis;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var db = _redis.GetDatabase();

        while (!stoppingToken.IsCancellationRequested)
        {
            // Try to become the leader (SET NX with expiry)
            var acquired = await db.StringSetAsync(
                LeaderKey,
                _instanceId,
                TimeSpan.FromMinutes(5),
                When.NotExists);

            if (acquired)
            {
                _logger.LogInformation("This instance is the leader, running task");

                try
                {
                    await DoScheduledWorkAsync(stoppingToken);
                }
                finally
                {
                    // Release leadership
                    await db.KeyDeleteAsync(LeaderKey);
                }
            }
            else
            {
                var leader = await db.StringGetAsync(LeaderKey);
                _logger.LogDebug("Another instance ({Leader}) is the leader", leader);
            }

            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}
```

## Jaettu lukitus kriittisiin osiin

Tehtävät, joita ei saa suorittaa samanaikaisesti eri tapausten välillä:

```csharp
public async Task ProcessWithLockAsync(CancellationToken cancellationToken)
{
    var db = _redis.GetDatabase();
    var lockKey = "locks:critical-task";
    var lockValue = _instanceId;

    // Try to acquire lock
    if (await db.LockTakeAsync(lockKey, lockValue, TimeSpan.FromMinutes(10)))
    {
        try
        {
            _logger.LogInformation("Lock acquired, processing...");
            await DoCriticalWorkAsync(cancellationToken);
        }
        finally
        {
            await db.LockReleaseAsync(lockKey, lockValue);
        }
    }
    else
    {
        _logger.LogDebug("Could not acquire lock, another instance is processing");
    }
}
```

## Milloin käytetään hajautettua koordinaatiota

- **Aikataulutetut tehtävät** - Vain yksi tapaus voi lähettää päivittäisen tiedotteen
- **Viivästysten käsittely tilauksen yhteydessä** - Varmista, että viestit käsitellään järjestyksessä
- **Resurssivaltainen toiminta** - Estä useita tapauksia ylivoimaiselta ulkoiselta API:ltä
- **Tietokantojen muuttoliikkeet** - Vain yksi tapaus saa pyörittää muuttoa startup-yrityksessä

Monimutkaisissa skenaarioissa (monivaiheiset työt, luotettava aikataulutus uudelleenkäynnistetyissä) harkitaan Hangfireä, joka käsittelee hajautettua lukitusta automaattisesti tietokantansa taustaosalla.

# Kun et käytä taustapalveluita

Ennen kuin sukellamme hienostuneempiin työkaluihin, kuten Hangfireen, puhutaan siitä, milloin *ei pitäisi* käytä taustapalveluita pääverkkosovelluksessasi.

## Merkkejä, jotka kannattaa jakaa erilliseen projektiin

Verkkosovelluksessasi toimivat taustapalvelut jakavat resursseja HTTP-pyyntöputken kanssa. Tämä voi aiheuttaa ongelmia:

### 1. Resurssisisältö

**Ongelma:** Taustapalvelusi kuluttaa merkittäviä prosessori-, muisti- tai tietokantayhteyksiä.

```csharp
// This will starve your web application
public class VideoTranscodingService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var video = await _queue.DequeueAsync();
            // This uses 100% of 4 CPU cores for 5 minutes
            await TranscodeVideoAsync(video);
        }
    }
}
```

**Kun verkkopyynnöt saapuvat transkoodauksen aikana, ne ovat hitaita, koska suorittimella on kiire.**

**Ratkaisu:** Siirry erilliseen työntekijäpalveluun:

```bash
# Your solution structure
/YourApp.Web          # ASP.NET Core web app - no background services
/YourApp.Worker       # .NET Worker Service - handles background work
/YourApp.Shared       # Shared models, interfaces
```

### 2. Erilaiset skaalausvaatimukset

**Ongelma:** Taustatyösi kaipaa erilaista skaalaa kuin verkkotasosi.

- **Verkkotaso:** Skaala HTTP-liikenteelle (voi tarvita 10 kertaa päivällä, 2 yöllä)
- **Taustataso:** Skaala jonon syvyyteen (voi tarvita yhden instanssin normaalisti, 20 erää käsiteltäessä)

Jos he ovat samassa prosessissa, heitä ei voi skaalata itsenäisesti.

**Esimerkki:**

```
09:00 - High web traffic, low background work → Need 10 web instances, 1 worker
14:00 - Newsletter time! Low web traffic, high background work → Need 2 web instances, 20 workers
```

Taustapalveluiden laittaminen web-sovellukseen tarkoittaa, että sinun pitäisi ajaa 20 verkkotapahtumaa vain käsitelläksesi uutiskirjeen ja tuhlataksesi resursseja.

### 3. Itsenäistyminen

**Ongelma:** Haluat tehdä verkkomuutoksia käynnistämättä taustapalveluita uudelleen (tai päinvastoin).

```csharp
// If this is in your web app, deploying a CSS change restarts the service
public class LongRunningImportService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // This import takes 2 hours
        await ImportMillionsOfRecordsAsync(stoppingToken);
    }
}
```

Jokainen käyttöönotto keskeyttää maahantuonnin. Siirrä se erilliseen työntekijäpalveluun, jota käytät itsenäisesti.

### 4. Erilaisia epäonnistumisverkkotunnuksia

**Ongelma:** Vika taustapalvelussasi kaataa koko verkkosovelluksen.

```csharp
// This null reference exception crashes your web app
public class BuggyBackgroundService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        string value = null;
        // Unhandled exception - takes down the whole app
        await ProcessAsync(value.Length);
    }
}
```

Jos taustatyö on erillisessä prosessissa, se voi kaatua ja käynnistyä uudelleen vaikuttamatta verkkopyyntöihin.

## Miten luoda taustapalveluja

Kun päätät jakautua, tässä on suositeltu arkkitehtuuri:

### Vaihtoehto 1: .NET-työntekijäpalvelu

Luo uusi projekti Worker Service -mallin avulla:

```bash
dotnet new worker -n YourApp.Worker
```

Rakenne:

```
/YourApp.Worker
  /Services
    VideoTranscodingService.cs
    EmailSenderService.cs
  /Program.cs
  /appsettings.json
```

Ohjelma.cs:

```csharp
var builder = Host.CreateApplicationBuilder(args);

// Register your background services
builder.Services.AddHostedService<VideoTranscodingService>();
builder.Services.AddHostedService<EmailSenderService>();

// Share configuration with web app
builder.Services.Configure<VideoConfig>(
    builder.Configuration.GetSection("Video"));

// Share database context
builder.Services.AddDbContext<YourDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

var host = builder.Build();
host.Run();
```

Käytössä erikseen:

```bash
# Web app on ports 80/443
/YourApp.Web → web-server-1, web-server-2, web-server-3

# Worker service doesn't listen on any port
/YourApp.Worker → worker-server-1, worker-server-2
```

### Vaihtoehto 2: Erillinen projekti jaetulla jonolla

Käytä viestijonoa verkon ja työntekijöiden erottamiseen toisistaan:

```mermaid
graph LR
    A[Web App] --> B[Message Queue]
    B --> C[Worker 1]
    B --> D[Worker 2]
    B --> E[Worker N]

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style B stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style C stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
    style D stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
    style E stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
```

Verkkosovellusjonot toimivat:

```csharp
// In your web controller
public class VideoController : ControllerBase
{
    private readonly IMessageQueue _queue;

    [HttpPost("upload")]
    public async Task<IActionResult> Upload(IFormFile video)
    {
        await _storage.SaveAsync(video);

        // Queue for processing - don't process in web app
        await _queue.PublishAsync(new VideoTranscodeJob
        {
            VideoId = video.Id,
            Priority = Priority.Normal
        });

        return Accepted(); // Return immediately
    }
}
```

Työntekijä kuluttaa jonosta:

```csharp
// In your worker service
public class VideoWorker : BackgroundService
{
    private readonly IMessageQueue _queue;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var job in _queue.SubscribeAsync<VideoTranscodeJob>(stoppingToken))
        {
            await TranscodeAsync(job);
        }
    }
}
```

**Suosittuja viestijonoja:**

- [**RabbitMQ**](https://www.rabbitmq.com/) - Suosituin, ominaisuusrikkain
- [**Azure-palvelubussi**](https://azure.microsoft.com/en-us/products/service-bus/) - Jos käytät Azurea
- [**AWS SQS**](https://aws.amazon.com/sqs/) - Jos olet AWS:ssä
- [**Redis-virtoja**](https://redis.io/docs/data-types/streams/) - Yksinkertaisempi, hyvä pienempään mittakaavaan

### Vaihtoehto 3: Useita erikoistuneita työntekijöitä

Monimutkaisissa järjestelmissä vastuu jaetaan seuraavasti:

```
/YourApp.Web              # HTTP requests only
/YourApp.EmailWorker      # Sends emails
/YourApp.VideoWorker      # Transcodes videos
/YourApp.ReportWorker     # Generates reports
/YourApp.Scheduler        # Runs scheduled jobs (Hangfire)
```

Jokainen työntekijä voi:

- Skaalaa itsenäisesti
- Käytössä itsenäisesti
- Käytä erilaisia resursseja (sähköpostityöntekijä tarvitsee SMTP:tä, videotyöntekijä tarvitsee GPU:ta)
- Erilainen seuranta ja hälytys

## Milloin säilytät taustapalveluita verkkosovelluksessasi

Edellä esitetystä huolimatta jotkin skenaariot ovat täysin sopivia prosessin aikana tarjottaville taustapalveluille:

### Kevyet määräaikaiset tehtävät

```csharp
// Fine to keep in web app
public class CacheWarmingService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _cache.WarmupAsync(); // Quick operation
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}
```

### Tapahtuman kuuntelijat

```csharp
// Fine to keep in web app
public class FileWatcherService : IHostedService
{
    // Reacts to events, doesn't consume significant resources
    private FileSystemWatcher _watcher;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _watcher = new FileSystemWatcher("/config");
        _watcher.Changed += OnConfigChanged;
        _watcher.EnableRaisingEvents = true;
        return Task.CompletedTask;
    }
}
```

### Kanavapohjaiset jonot (ei-kriittiset työt)

```csharp
// Fine to keep in web app if work is quick and not critical
public class EmailQueueService : BackgroundService
{
    // Sends emails in background, but each email takes < 1 second
    // If the app restarts, losing a few queued emails is acceptable
}
```

### Käynnistyskoordinointi

```csharp
// Fine to keep in web app
public class WarmupService : IHostedService
{
    // Runs once at startup, then does nothing
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await _database.WarmupConnectionPoolAsync();
        await _cache.LoadCriticalDataAsync();
    }
}
```

## Päätös Matrix

Ominaispiirre: Pysy Web-sovelluksessa Siirry Worker Service -palveluun
|---------------|-----------------|------------------------|
CPU:n käyttö per toiminto < 100 ms > 1 sekunti
Toimintakohtainen muisti < 10 MB > 100 MB
Taajuus jaksoittain (minuutteja/tunteja) Jatkuva tai suuri taajuus
Kriittisyys Kriittisyys Kriittisyys Kriittisyys Kriittisyys Kriittisyys Kriittisyys Kriittisyys Kriittisyys Kriittisyys Kriittisyys Kriittisyys Kriittisyys Kriittisyys Kriittisyys Kriittisyys
Kesto Sekunteja pöytäkirjoista tuntiin
Verkkoliikenteen asteikot Työn jonon syvyys
Esimerkki: Cache-lämpeneminen, videonkäsittelyn konfigurointi, suuri tuonti

## Reaalimaailman esimerkki: Blog Platform

Bloggausalustalla, jonka koodia tutkimme osassa 2:

**Pidetty web-sovelluksessa:**

- `MarkdownDirectoryWatcherService` - Kevyt tiedostojen valvoja
- `UmamiBackgroundSender` - Nopeat analytiikkatapahtumat
- `EmailSenderHostedService` - Pieni määrä, ei-kriittinen
- `MarkdownReAddPostsService` - Vain käynnistys, kokoonpano sovitettu

**Siirrytään työntekijäpalveluun, jos mittakaava kasvaa:**

- `BrokenLinkCheckerBackgroundService` - Tekee monia HTTP-pyyntöjä
- `SemanticIndexingBackgroundService` - Kutsuu ulkoista upotettavaa API-rajapintaa

**Jo erillisessä palvelussa:**

- `Mostlylucid.SchedulerService` - Hangfire-kojelauta ja uutiskirjeen lähetys

Tämä on pragmaattinen lähestymistapa: aloita yksinkertainen (prosessin aikana), jakaudu, kun sinulla on todisteita, joita tarvitset.

# Perusasioiden takana: Hangfire

Kun otetaan huomioon, että `IHostedService` sekä `BackgroundService` Ne ovat erinomaisia omistamillesi ja hallitsemillesi palveluille, joskus tarvitaan hienostunutta aikataulua. [Hangfire](https://www.hangfire.io/) Tule sisään.

Hangfire tarjoaa:

- **Jatkuvat työjonot** - Jobs selviytyy sovelluksen uudelleenkäynnistämisestä
- **Toistuvat työt** - Cron-tyylinen aikataulu
- **Dashboard UI** - Katso, mikä on käynnissä, mikä on epäonnistunut, yritä uudelleen töitä
- **Jaettu suoritus** - Useat palvelimet voivat käsitellä samaa työjonoa
- **Automaattiset retriikit** - Hylätyt työt yritetään automaattisesti uudelleen eksponentiaalisella taklauksella

Tässä yksinkertainen esimerkki:

```csharp
// In Program.cs
builder.Services.AddHangfire(config => config
    .UsePostgreSqlStorage(connectionString)
    .UseRecommendedSerializerSettings());

builder.Services.AddHangfireServer();

var app = builder.Build();

// Schedule recurring jobs
app.UseHangfireDashboard();
app.Services.GetRequiredService<IRecurringJobManager>()
    .AddOrUpdate<NewsletterService>(
        "send-daily-newsletter",
        x => x.SendDailyNewsletter(),
        Cron.Daily(17)); // 5 PM every day
```

Palveluksesi on ihan normaalia:

```csharp
public class NewsletterService
{
    private readonly IEmailService _emailService;
    private readonly ISubscriberRepository _subscribers;

    public NewsletterService(
        IEmailService emailService,
        ISubscriberRepository subscribers)
    {
        _emailService = emailService;
        _subscribers = subscribers;
    }

    public async Task SendDailyNewsletter()
    {
        var subscribers = await _subscribers.GetDailySubscribersAsync();

        foreach (var subscriber in subscribers)
        {
            await _emailService.SendNewsletterAsync(subscriber);
        }
    }
}
```

Hangfiren kahvat:

- Työn sujumisen varmistaminen aikataulun mukaisesti
- Yritä uudelleen, jos se epäonnistuu
- Toteutushistorian tallentaminen
- Kojelaudan tarjoaminen kaiken seuraamiseksi

```mermaid
graph TD
    A[Hangfire Server] --> B{Check Schedule}
    B -->|Job Due| C[Dequeue Job]
    C --> D[Execute Job Method]
    D -->|Success| E[Mark Complete]
    D -->|Failure| F[Retry with Backoff]
    F --> G{Max Retries?}
    G -->|No| C
    G -->|Yes| H[Mark Failed]
    E --> I[Update Dashboard]
    H --> I
    I --> B

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style D stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style E stroke:#059669,stroke-width:3px,color:#10b981
    style H stroke:#dc2626,stroke-width:3px,color:#ef4444
```

**Milloin Hangfirea käytetään:**

- Tarvitset sinnikkäitä työjonoja, jotka selviävät uudelleenkäynnistämisestä
- Haluat kojelaudan, jolla voit seurata ja manuaalisesti laukaista työtehtäviä
- Tarvitset jaetun työnkäsittelyn useille palvelimille
- Haluat sisäänrakennetun logiikan ja epäonnistumisen käsittelyn
- Toistuvien tehtävien aikataulut kaipaavat kronaattityyliä

**Milloin pysyt IHostedService/BackgroundService -palvelussa:**

- Palvelun elinkaarta pitää kontrolloida tarkasti
- Palvelusi on reagoitava tapahtumiin reaaliajassa
- Haluat minimoida riippuvuudet
- Rakennat yksinkertaista määräaikaista tehtävää, joka ei vaadi sinnikkyyttä

# Muita vaihtoehtoja

Hangfire on suosittu, mutta muita kirjastoja kannattaa harkita:

[**Quartz.NET**](https://www.quartz-scheduler.net/):

- Joustavampi aikataulu kuin Hangfire
- Tukee kronanilmauksia ja kalenteripohjaista aikataulutusta
- Voi säilyä useissa tietokannoissa
- Monimutkaisempi API, mutta tehokkaampi

[**Masstransit**](https://masstransit.io/)/[**NServiceBus**](https://particular.net/nservicebus):

- Täysipainoiset viestibussien toteutukset
- Parempia hajautettuja järjestelmiä ja mikropalveluja
- Support sagas (pitkän aikavälin työnkulku)
- Steeper-oppimiskäyrä

[**Azure-toiminnot**](https://azure.microsoft.com/en-us/products/functions/)/[**AWS Lambda**](https://aws.amazon.com/lambda/):

- Jos olet pilvissä, pidä palvelinta
- Palkka per suoritus sen sijaan, että palvelu pidettäisiin käynnissä
- Automaattinen skaalaus
- Kylmän alun latenssia

# Yhteenveto

Osassa 1 olemme käsitelleet ASP.NET Coren taustapalveluiden perustavanlaatuisia lähestymistapoja:

1. **IHostedService** - Perustus, maksimaalinen joustavuus
2. **Taustapalvelu** - Kätevä pohjaluokka kestosilmukoille
3. **Käynnistyskoordinointi** - Palvelukset odottavat toisiaan
4. **Hajautettu koordinointi** - Redisin käyttö monitoimisiin skenaarioihin
5. **Hangfire** - Kun tarvitset sinnikkäitä töitä ja hienostunutta aikataulua

Tärkeimmät opetukset:

- **Täydennä aina kanavat ja peruuta kuponki StopAsyncissä**
- **Päätä, pitääkö StartAsync estää vai palauttaa välittömästi**
- **Hoitakaa operaatioCanceledExclusion sulavasti**
- **Käytä Tehtävää.Kun kaikki, joilla on sammutustunnus, kunnioittavat pysäytysaikoja**

Sisään [2 osa](/blog/background-services-in-aspnetcore-part2)Tutustumme tosimaailman toteutuksiin tuotantoblogin alustalta:

- Tiedostojärjestelmän tarkkailijat, jotka synkronoivat tiedostot tietokantaan
- Sähköpostin lähettäjät uudelleenyrittäjät ja virrankatkaisijat
- Analyyttiset tapahtumajonot, joita erät pyytävät
- Semanttiset hakuindeksaattorit, jotka käsittelevät sisältöä asynkronisesti
- Rikkinäiset linkkien tarkistajat, jotka säännöllisesti validoivat ulkoisia URL-osoitteita

Nämä esimerkit osoittavat osan 1 kuviot toiminnassa, mukaan lukien startup-koordinointikuvio ja asianmukainen pysäytyskäsittely.

# Lisää luettavaa

- [Microsoft Docs: Taustatehtäviä isännöidyillä palveluilla](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services)
- [Hangfire-dokumentaatio](https://docs.hangfire.io/)
- [Kanavat C#:ssä](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels)
- [Quartz.NET-dokumentaatio](https://www.quartz-scheduler.net/)
- [StackExchange.Redis](https://stackexchange.github.io/StackExchange.Redis/) .NET:n Redis-asiakas