Πότε, πού, γιατί και πώς το χρησιμοποιούμε, και ποια είναι η πραγματική χρησιμότητά του.
Το πρόβλημα που λύνει
Σε κάθε σύγχρονη εφαρμογή, ειδικά σε web APIs, services και συστήματα με ασύγχρονες εργασίες, υπάρχουν λειτουργίες που ξεκινούν αλλά δεν είναι εγγυημένο ότι πρέπει να ολοκληρωθούν. Ένα HTTP request μπορεί να ακυρωθεί επειδή ο χρήστης έκλεισε τη σελίδα, επειδή ο browser διέκοψε τη σύνδεση, επειδή έληξε ένα timeout, επειδή ο load balancer έκλεισε το socket ή επειδή το σύστημα αποφάσισε ότι η εργασία δεν έχει πλέον αξία να συνεχιστεί. Αν ο κώδικας συνεχίσει να εκτελείται αδιάφορα, τότε η εφαρμογή συνεχίζει να καταναλώνει CPU, μνήμη και συνδέσεις βάσης για μια εργασία που στην πράξη έχει ήδη “πεθάνει” επιχειρησιακά.
Το CancellationToken υπάρχει για να λύσει αυτό ακριβώς το πρόβλημα: να δώσει έναν καθαρό και τυποποιημένο τρόπο ώστε ο κώδικας να μπορεί να σταματήσει συντονισμένα, όταν το σύστημα ή ο χρήστης ζητήσει ακύρωση. Δεν είναι μηχανισμός “kill thread”. Είναι πρωτόκολλο συνεργασίας ανάμεσα στον caller και στον callee: ο caller δηλώνει ότι δεν θέλει πια να συνεχιστεί η εργασία, και ο callee επιλέγει ασφαλή σημεία μέσα στη ροή του για να τερματίσει.
Τι είναι το CancellationToken και τι μεταφέρει
Το CancellationToken είναι ένα μικρό αντικείμενο που μεταφέρεται ως παράμετρος σε μεθόδους. Το token δεν “σταματάει” μόνο του τίποτα. Μεταφέρει ένα σήμα που λέει αν έχει ζητηθεί ακύρωση. Οποιαδήποτε μέθοδος το λαμβάνει μπορεί να το ελέγξει και να σταματήσει ομαλά. Η ακύρωση είναι συνεπώς συνεργατική. Αυτή η επιλογή είναι βαθιά σχεδιαστική: το .NET επιλέγει να μη διακόπτει βίαια μια λειτουργία γιατί αυτό μπορεί να αφήσει μισογραμμένα δεδομένα, μισά κλειδωμένους πόρους ή corrupt state.
Η βασική πρακτική έννοια είναι ότι το token επιτρέπει στον κώδικα να εγκαταλείψει εγκαίρως, να κάνει cleanup και να μην κρατάει πόρους άσκοπα.
Πότε χρησιμοποιείται και γιατί είναι σημαντικό
Σε web εφαρμογές, το CancellationToken αντιστοιχεί πολύ συχνά στο lifetime του HTTP request. Όταν ο client φύγει, όταν η σύνδεση χαθεί, ή όταν ο server αποφασίσει ότι το request δεν πρέπει να συνεχίσει, το framework σηματοδοτεί το cancellation. Αν ο κώδικάς μας το αγνοεί, τότε εκείνη η δουλειά συνεχίζει “ορφανή”, και αυτό σε συστήματα με φόρτο οδηγεί σε επιβάρυνση, περισσότερα timeouts, περισσότερα locks, και τελικά σε ανεξήγητη πτώση απόδοσης. Το CancellationToken είναι λοιπόν εργαλείο ελέγχου πόρων και σταθερότητας.
Σε background jobs και services, η ακύρωση είναι εξίσου κρίσιμη. Όταν κλείνει η εφαρμογή ή όταν γίνεται redeploy, το hosting environment ζητά από τις εργασίες να σταματήσουν. Αν αγνοήσουν το cancellation, το shutdown καθυστερεί, οι εργασίες κόβονται βίαια ή μένουν στη μέση, και η αξιοπιστία πέφτει.
Πού περνάμε CancellationToken στον κώδικα
Η σωστή πρακτική είναι να περνάμε CancellationToken σε όλη την αλυσίδα κλήσεων, από το εξωτερικό boundary προς τα μέσα. Αυτό σημαίνει ότι σε APIs, controllers, handlers και services δέχονται CancellationToken ct και το προωθούν προς τις εξαρτήσεις τους. Δεν δημιουργούμε νέο token αυθαίρετα μέσα στη μέθοδο. Το προωθούμε.
Παράδειγμα σε controller:
public async Task<IActionResult> UpdateOrder(UpdateOrderRequest request, CancellationToken ct)
{
await _handler.HandleAsync(request, ct);
return Ok();
}
Παράδειγμα σε handler:
public async Task HandleAsync(UpdateOrderRequest request, CancellationToken ct)
{
var order = await _db.Orders.FirstAsync(x => x.Id == request.Id, ct);
order.Status = request.Status;
await _db.SaveChangesAsync(ct);
}
Η σημασία εδώ είναι ότι το EF Core λαμβάνει το token. Αν το request ακυρωθεί, το query ή το save μπορεί να ακυρωθεί και να απελευθερώσει πόρους.
Πώς το χρησιμοποιούμε σε βάθος μέσα σε μια μέθοδο
Η πιο καθαρή μορφή χρήσης του CancellationToken είναι να το περνάμε σε async APIs που το υποστηρίζουν. Αυτό από μόνο του είναι το 80% της αξίας. Υπάρχουν όμως περιπτώσεις όπου η δουλειά μας είναι “CPU heavy” ή είναι ένας δικός μας βρόχος που δεν καλεί εξωτερικές async μεθόδους. Εκεί πρέπει να ελέγξουμε εμείς το token και να διακόψουμε.
Παράδειγμα σε loop:
public async Task ProcessItemsAsync(IEnumerable<Item> items, CancellationToken ct)
{
foreach (var item in items)
{
ct.ThrowIfCancellationRequested();
await ProcessItemAsync(item, ct);
}
}
Το ThrowIfCancellationRequested() είναι ο τυπικός τρόπος να σταματήσεις τη ροή με καθαρό τρόπο. Θα πετάξει OperationCanceledException, το οποίο το framework γνωρίζει ότι σημαίνει “ακύρωση” και όχι “σφάλμα”.
Σε πιο βαριές εργασίες όπου έχεις checkpoints, ο σωστός σχεδιασμός είναι να αποφασίσεις σε ποια σημεία είναι ασφαλές να σταματήσεις. Δεν θέλεις να διακόψεις στη μέση μιας μη αντιστρέψιμης ενέργειας. Θέλεις να διακόψεις ανάμεσα σε βήματα.
Τι γίνεται όταν ακυρώνεται ένα request και πώς το βλέπει η εφαρμογή
Σε ASP.NET Core, όταν ακυρωθεί ένα request, το CancellationToken που σου δίνει το framework γίνεται canceled. Αν το περάσεις σε database calls, αυτά μπορούν να σταματήσουν. Αν το αγνοήσεις, η δουλειά συνεχίζει. Η ακύρωση δεν είναι “μαγικό φρένο”. Είναι σήμα.
Η σωστή αντιμετώπιση είναι να αφήσεις το OperationCanceledException να περάσει προς τα πάνω, ή να το χειριστείς μόνο όταν έχεις λόγο. Οι περισσότεροι μηχανισμοί του framework το μετατρέπουν σε σωστή συμπεριφορά χωρίς να το θεωρούν error.
Συνδυασμός ακύρωσης και timeouts
Σε πολλά συστήματα, θέλουμε όχι μόνο να ακυρώνουμε όταν ακυρώθηκε το request, αλλά και να βάζουμε και δικά μας timeouts. Αυτό γίνεται με CancellationTokenSource. Δημιουργείς ένα token που ακυρώνεται μετά από συγκεκριμένο χρόνο και το συνδυάζεις με το token του request.
Παράδειγμα:
public async Task<IActionResult> GetReport(CancellationToken ct)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(5));
var result = await _service.GenerateReportAsync(cts.Token);
return Ok(result);
}
Εδώ συνδυάζονται δύο λόγοι ακύρωσης. Αν ο χρήστης φύγει, ακυρώνεται. Αν περάσουν 5 δευτερόλεπτα, ακυρώνεται. Αυτό είναι ιδιαίτερα χρήσιμο σε endpoints που δεν θέλεις να “κρέμονται”.
Τι σχέση έχει με Clean Architecture και SOLID
Σε καθαρή αρχιτεκτονική, ο ρόλος του CancellationToken είναι να ταξιδεύει από το boundary προς το εσωτερικό, χωρίς το domain να εξαρτάται από frameworks. Το token είναι μέρος του BCL και θεωρείται αποδεκτό να περνάει στο application layer και στα infrastructure services. Είναι καλό να μην το βάζεις βαθιά στο καθαρό domain model ως απαίτηση για να λειτουργήσει μια οντότητα, αλλά είναι απόλυτα φυσιολογικό να υπάρχει σε application services, repositories και external calls.
Ο κανόνας που κρατάει το design καθαρό είναι ότι δεν κάνεις την επιχειρησιακή λογική σου να εξαρτάται από τον τρόπο ακύρωσης. Απλώς επιτρέπεις στο runtime να ακυρώσει τη ροή όταν χρειαστεί.
Παράδειγμα πλήρους ροής με DI
Service:
public interface IReportService
{
Task<Report> CreateAsync(long orderId, CancellationToken ct);
}
Implementation:
public class ReportService : IReportService
{
private readonly EndysisDbContext _db;
public ReportService(EndysisDbContext db) => _db = db;
public async Task<Report> CreateAsync(long orderId, CancellationToken ct)
{
var order = await _db.Orders.FirstAsync(x => x.Id == orderId, ct);
ct.ThrowIfCancellationRequested();
var report = new Report { OrderId = order.Id };
_db.Reports.Add(report);
await _db.SaveChangesAsync(ct);
return report;
}
}
DI registration:
services.AddScoped<IReportService, ReportService>();
Controller:
[HttpPost("orders/{id}/report")]
public async Task<IActionResult> CreateReport(long id, CancellationToken ct)
{
var report = await _reportService.CreateAsync(id, ct);
return Ok(report);
}
Το σημαντικό εδώ είναι ότι η ακύρωση περνάει καθαρά και χωρίς “κόλπα” μέχρι τη βάση.
Να θυμάσαι..
Να θυμάσαι ότι το CancellationToken είναι ένας μηχανισμός προστασίας του συστήματος από άσκοπη εργασία. Δεν υπάρχει για να κάνει τον κώδικα πιο “μοντέρνο”, αλλά για να διασφαλίζει ότι η εφαρμογή μπορεί να σταματήσει γρήγορα όταν ένα αποτέλεσμα δεν έχει πλέον αξία. Να θυμάσαι ότι δεν σταματάει τίποτα μόνο του. Είναι σήμα που πρέπει να το σεβαστεί ο κώδικας, είτε περνώντας το σε async APIs είτε ελέγχοντάς το σε δικές σου διαδικασίες. Να θυμάσαι ότι η σωστή διάδοση του token σε όλη την αλυσίδα κλήσεων είναι δείγμα καλής αρχιτεκτονικής, γιατί κρατά το σύστημα συνεπές και προβλέψιμο σε περιπτώσεις ακύρωσης. Και να θυμάσαι ότι η ακύρωση δεν είναι αποτυχία· είναι σωστός και ελεγχόμενος τερματισμός, ώστε οι πόροι να επιστρέφουν εκεί που ανήκουν και η εφαρμογή να παραμένει σταθερή υπό φόρτο.
Top comments (0)