RE:NODE

აპლიკაციები12 წუთის საკითხავი

.NET worker სერვისები და background job-ები: hosted-იდან Hangfire-მდე

ფონური სამუშაოს სწორად გაშვება .NET-ში: BackgroundService, PeriodicTimer, პროცესის შიდა რიგები Channels-ით, Quartz.NET, Hangfire, შეცდომების დამუშავება და სუფთა გამორთვა.

0 მკითხველი

ფონური სამუშაო .NET-ში BackgroundService-ით იწყება: კლასი ერთი მეთოდით, ExecuteAsync, რომელსაც Generic Host აპლიკაციასთან ერთად უშვებს და აპლიკაციის გაჩერებისას აუქმებს. ამოცანისთვის "ყოველ ხუთ წუთში გააკეთე ეს" BackgroundService PeriodicTimer-ით სავსებით საკმარისია. ამოცანისთვის "გააკეთე ეს მალე, ისე რომ HTTP მოთხოვნა არ ალოდინო" BackgroundService-ისთვის მკვებავი შეზღუდული Channel სავსებით საკმარისია. ბიბლიოთეკას მაშინ მიმართავ, როცა სამუშაომ restart-ს უნდა გადაურჩეს ან კალენდარს უნდა მიჰყვეს: Quartz.NET cron-ის სტილის განრიგებისთვის, Hangfire მონაცემთა ბაზაში შენახული job-ებისთვის ხელახალი ცდებით და dashboard-ით. რომელიც არ უნდა გამოიყენო, ორი რამ, რაც წყვეტს, იმუშავებს თუ არა ის production-ში, ერთი და იგივეა - რა ხდება, როცა job გამონაკლისს ისვრის, და რა ხდება, როცა პროცესს job-ის შუაში გაჩერებას უბრძანებენ.

ეს პოსტი თითოეულ მათგანს აწყობს იმის მიხედვით, რამდენ მექანიზმს საჭიროებენ.

ჰოსტინგის მოდელი#

ყოველი თანამედროვე .NET აპლიკაცია - ვებ, worker თუ კონსოლი ჰოსტით - Generic Host-ზე მუშაობს, ჰოსტი კი hosted სერვისებს უშვებს. IHostedService არის ინტერფეისი: StartAsync აპლიკაციის გაშვებისას, StopAsync მისი გაჩერებისას. BackgroundService არის საბაზო კლასი, რომელსაც თითქმის ყველა მის ნაცვლად იყენებს, რადგან ის ამას ერთ ხანგრძლივ მეთოდად აქცევს cancellation ტოკენით.

bash
$ dotnet new worker -n Reports.Worker
Program.cs
var builder = Host.CreateApplicationBuilder(args);builder.Services.AddHostedService<CleanupWorker>();builder.Build().Run();

ASP.NET Core აპლიკაციაში ეს იგივე ხაზია, builder.Services.AddHostedService<CleanupWorker>(), და worker ვებ პროცესის შიგნით მუშაობს მოთხოვნების pipeline-ის გვერდით. ეს კომპრომისი არ არის. პატარა აპლიკაციების უმეტესობისთვის ეს სწორი ადგილია, რადგან worker ვებ აპლიკაციასთან ერთად იზიარებს კონფიგურაციას, ლოგირებას და სერვისებს, და deploy-ისთვის მხოლოდ ერთი პროცესია.

Hosted სერვისები რეგისტრაციის რიგით ეშვება, და ძველ ვერსიებში გაშვება თანმიმდევრული იყო: ExecuteAsync-ში პირველ await-მდე კოდი გაშვების გზაზე ეშვებოდა და აყოვნებდა ყველაფერს, რაც მის შემდეგ იყო რეგისტრირებული. .NET 10-მა BackgroundService შეცვალა ისე, რომ მთელ ExecuteAsync-ს ფონზე უშვებს. ადრეულ ვერსიებზე სინქრონული ნაწილი მოკლე დატოვე, ან მეთოდი await Task.Yield()-ით დაიწყე. .NET 8-მა ასევე დაამატა HostOptions.ServicesStartConcurrently და IHostedLifecycleService, StartingAsync, StartedAsync, StoppingAsync და StoppedAsync hook-ებით იმ სერვისებისთვის, რომლებსაც უფრო დეტალური კონტროლი სჭირდებათ.

Worker, რომელიც ტაიმერით მუშაობს#

კლასიკური ფორმა არის ციკლი PeriodicTimer-ით:

CleanupWorker.cs
public sealed class CleanupWorker(    IServiceScopeFactory scopes,    ILogger<CleanupWorker> log) : BackgroundService{    protected override async Task ExecuteAsync(CancellationToken stoppingToken)    {        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));        do        {            try            {                await using var scope = scopes.CreateAsyncScope();                var db = scope.ServiceProvider.GetRequiredService<ShopDb>();                var removed = await db.Carts                    .Where(c => c.UpdatedAt < DateTime.UtcNow.AddDays(-7))                    .ExecuteDeleteAsync(stoppingToken);                log.LogInformation("Removed {Count} stale carts", removed);            }            catch (Exception ex) when (ex is not OperationCanceledException)            {                log.LogError(ex, "Cart cleanup failed");            }        }        while (await timer.WaitForNextTickAsync(stoppingToken));    }}

დეტალები, რომლებსაც მნიშვნელობა აქვს:

  • Scope-ები. hosted სერვისი singleton-ია. scoped სერვისების, მაგალითად EF Core-ის DbContext-ის, მასში პირდაპირ inject-ი შეუძლებელია - ჰოსტი გაშვებისას უარს ამბობს - და მათი შენახვა მისი მთელი სიცოცხლის განმავლობაში მაინც არ ღირს, რადგან change tracker უსასრულოდ იზრდება. შექმენი scope სამუშაოს ყოველ ერთეულზე IServiceScopeFactory-ით, როგორც ზემოთაა.
  • `PeriodicTimer` არ გადაფარავს. თუ ერთი გაშვება პერიოდზე მეტხანს გრძელდება, შემდეგი tick ელოდება; გაშვებები ერთმანეთზე არასოდეს გროვდება. ჩვეულებრივ ეს ის არის, რაც გინდა, და ეს არ ეხება System.Threading.Timer-ს, რომელიც განრიგით ისვრის, დასრულდა თუ არა ბოლო callback.
  • `WaitForNextTickAsync` გაუქმებისას გამონაკლისს ისვრის. როცა ჰოსტი ჩერდება, ტოკენი უქმდება და ლოდინი OperationCanceledException-ს ისვრის, რაც ციკლს ამთავრებს. BackgroundService ამას ჩვეულებრივ გაჩერებად აღიქვამს.
  • დაიჭირე ყოველ იტერაციაზე. ციკლის შიგნით try ნიშნავს, რომ ერთი ჩავარდნილი გაშვება ლოგში იწერება და შემდეგი მაინც ხდება. მის გარეშე პირველი გამონაკლისი worker-ს ამთავრებს - იხილე ქვემოთ.

ეს გასაკვირად ბევრ რამეს ფარავს: ქეშის გათბობას, გასუფთავებას, გარე API-ის polling-ს, რიგში მდგარი ელფოსტის გაგზავნას, ლიდერბორდის ხელახლა გამოთვლას. Background job-ები პატარა სერვერზე განიხილავს, რომელი მათგანი ეკუთვნის საერთოდ აპლიკაციას.

რიგები პროცესის შიგნით Channels-ით#

მოთხოვნით გამოწვეული სამუშაოსთვის - მისასალმებელი ელფოსტის გაგზავნა, ატვირთულის ზომის შეცვლა, ნელი webhook-ის გამოძახება - მოთხოვნა მაშინვე უნდა დაბრუნდეს, სამუშაო კი მის უკან უნდა მოხდეს. System.Threading.Channels ამისთვის ჩაშენებული, ალოკაციებით მსუბუქი რიგია:

csharp
public sealed class EmailQueue{    private readonly Channel<int> _channel = Channel.CreateBounded<int>(        new BoundedChannelOptions(500) { FullMode = BoundedChannelFullMode.Wait });    public ValueTask EnqueueAsync(int userId, CancellationToken ct) =>        _channel.Writer.WriteAsync(userId, ct);    public IAsyncEnumerable<int> ReadAllAsync(CancellationToken ct) =>        _channel.Reader.ReadAllAsync(ct);}public sealed class EmailWorker(EmailQueue queue, IServiceScopeFactory scopes,    ILogger<EmailWorker> log) : BackgroundService{    protected override async Task ExecuteAsync(CancellationToken stoppingToken)    {        await foreach (var userId in queue.ReadAllAsync(stoppingToken))        {            try            {                await using var scope = scopes.CreateAsyncScope();                var sender = scope.ServiceProvider.GetRequiredService<IWelcomeMailer>();                await sender.SendAsync(userId, stoppingToken);            }            catch (Exception ex) when (ex is not OperationCanceledException)            {                log.LogError(ex, "Welcome mail for {UserId} failed", userId);            }        }    }}

დაარეგისტრირე EmailQueue singleton-ად, worker კი hosted სერვისად; endpoint EnqueueAsync-ს იძახებს და ბრუნდება.

channel შეზღუდული გახადე. შეუზღუდავი channel სამუშაოს იმაზე სწრაფად იღებს, ვიდრე მისი დამუშავება შეგიძლია, სანამ მეხსიერება არ ამოიწურება; შეზღუდული FullMode = Wait-ით ამის ნაცვლად მწარმოებლებს ანელებს, რაც პატიოსანი ქცევაა. რიგში პატარა რამეები ჩადე - ID და არა entity ან ფაილი - რომ რიგის მეხსიერება პროგნოზირებადი დარჩეს.

სუსტი მხარე ნათლად გაიაზრე: მეხსიერებაში არსებული რიგი პროცესთან ერთად კვდება. ყველაფერი, რაც მასში ელოდება, როცა აპლიკაცია რესტარტდება ან ვარდება, იკარგება. მისასალმებელი ელფოსტისთვის ეს ხშირად მისაღებია; გადახდის დადასტურებისთვის - არა. ეს ზღვარი - "შემიძლია ამის დაკარგვა?" - არის ის წერტილი, სადაც მდგრადობა გჭირდება, რაც ნიშნავს Hangfire-ს, მონაცემთა ბაზის ცხრილს, რომელსაც polling-ით ამოწმებ, ან რიგის სერვერს. Job რიგები Valkey-ით რიგის სერვერის ვარიანტს განიხილავს.

შეცდომები და რა კლავს ჰოსტს#

.NET 6-იდან გამონაკლისი, რომელიც ExecuteAsync-ს გაექცევა, მთელ ჰოსტს აჩერებს. პარამეტრია HostOptions.BackgroundServiceExceptionBehavior, და მისი ნაგულისხმევი მნიშვნელობაა StopHost:

csharp
builder.Services.Configure<HostOptions>(o =>{    o.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.StopHost;});

ეს ნაგულისხმევი სწორია, ხოლო ალტერნატივა, Ignore, ჩვეულებრივ არასწორი. Ignore-ით worker ჩუმად ჩერდება, სანამ აპლიკაცია მოთხოვნების მომსახურებას აგრძელებს, და ვერაფერი გეტყვის, რომ გასუფთავება სამი კვირის წინ შეწყდა. StopHost-ით პროცესი გადის, გამონაკლისი ლოგში იწერება და პლატფორმა მას რესტარტავს - ხილულია და თავად აღდგება, თუ ჩავარდნა დროებითი იყო.

რეალური გამოსწორებაა ზემოთ ნაჩვენები try/catch ყოველ იტერაციაზე: ერთმა ცუდმა ჩანაწერმა ერთი იტერაცია უნდა დაგიჯდეს და არა პროცესი. პროცესის დამასრულებელი ჩავარდნები დაიტოვე იმისთვის, რაც მართლა ფატალურია, მაგალითად არარსებული კონფიგურაციისთვის.

ყველაფრის დაჭერას საკუთარი ჩავარდნის რეჟიმი აქვს: worker, რომელიც ყოველ იტერაციაზე ვარდება, ყოველ ჯერზე შეცდომას ლოგავს და არასოდეს ჩერდება. ლოგს არავინ კითხულობს, და job ფაქტობრივად კვირებია მკვდარია. ბოლო წარმატებული გაშვების დრო singleton-ში ჩაიწერე და გამოაჩინე health check-ით, რომელიც unhealthy-ს აცხადებს, როცა ბოლო წარმატება, ვთქვათ, სამ პერიოდზე ძველია. მაშინ მონიტორინგის შემოწმება, ან უბრალოდ health endpoint, რომელსაც deploy-ების შემდეგ ისედაც იძახებ, გეტყვის, რომ job გაჭედილია, სანამ მომხმარებელი გეტყვის. მონიტორინგი, რომელიც რაღაცას გეუბნება განიხილავს, რაზე გააკეთო alert-ი და რა დატოვო მშვიდად.

RE:NODE-ზე პროცესი, რომელიც გადის, რესტარტდება, ხოლო crash watcher ითვლის restart-ებს, რომლებიც არავის მოუთხოვია: საათში სამი სერვერის გვერდზე გაფრთხილებას აჩენს და ავტომატურად ხსნის ticket-ს, ექვსი კი შეჩერებამდე მიდის. ამიტომ worker, რომელიც ყოველ გაშვებაზე გამონაკლისს ისვრის, სწრაფად შესამჩნევი ხდება - მაგრამ ჯობია ყოველ ელემენტზე დაიჭირო და ჩაწერო ლოგში, ვიდრე ამას დაეყრდნო.

გამორთვა: რა ხდება გაჩერებისა და deploy-ის დროს#

როცა ჰოსტს გაჩერებას სთხოვენ - deploy, restart, გაჩერება პანელიდან - ის აუქმებს stoppingToken-ს და ელოდება, სანამ ყოველი hosted სერვისი დასრულდება, HostOptions.ShutdownTimeout-მდე. ნაგულისხმევი .NET 6-იდან 30 წამია. ამის შემდეგ ჰოსტი ლოდინს წყვეტს და პროცესი გადის, საჭიროების შემთხვევაში job-ის შუაში.

ამიტომ ყოველმა job-მა უნდა გაუმკლავდეს შეწყვეტას:

  1. ტოკენი ყველგან გადაეცი. მონაცემთა ბაზის გამოძახებები, HTTP გამოძახებები და დაყოვნებები, რომლებიც stoppingToken-ს იღებენ, მისი გაუქმებისას სწრაფად სრულდება. ციკლი, რომელიც მას აიგნორებს, timeout-მდე მუშაობს და შემდეგ მაინც წყდება.
  2. გახადე სამუშაო idempotent. შუაში შეწყვეტილი job თავიდან გაეშვება. ერთი და იმავე ელფოსტის ორჯერ გაგზავნა ან ორჯერ ჩამოჭრა დიზაინით უნდა იყოს შეუძლებელი: ჩაიწერე, რა გაკეთდა, და შეამოწმე გაკეთებამდე.
  3. ერთეულები პატარა დატოვე. job, რომელიც 10,000 სტრიქონს ერთ ტრანზაქციაში ამუშავებს, შეწყვეტისას ყველაფერს კარგავს. რამდენიმე ასეულიანი batch-ები, თითოეული დაკომიტებული, მაქსიმუმ ერთ batch-ს კარგავს.

თუ პროცესს თავად უშვებ, start ბრძანებამ გაჩერების სიგნალი პირდაპირ .NET პროცესს უნდა გადასცეს - exec dotnet App.dll და არა shell wrapper - თორემ ჰოსტი მას ვერასოდეს გაიგებს და სუფთად ვერ გაითიშება. Graceful shutdown და health check-ები სიგნალების მხარეს ზოგადად განიხილავს.

Quartz.NET განრიგებისთვის#

როცა სამუშაო კონკრეტულ დროს უნდა მოხდეს - ყოველ ღამე 03:00-ზე, თვის პირველ ორშაბათს - და არა ინტერვალით, გამოიყენე scheduler. Quartz.NET დამკვიდრებული ვარიანტია:

bash
$ dotnet add package Quartz.Extensions.Hosting
csharp
builder.Services.AddQuartz(q =>{    var key = new JobKey("nightly-report");    q.AddJob<NightlyReportJob>(o => o.WithIdentity(key));    q.AddTrigger(t => t.ForJob(key).WithCronSchedule("0 0 3 * * ?"));});builder.Services.AddQuartzHostedService(o => o.WaitForJobsToComplete = true);[DisallowConcurrentExecution]public sealed class NightlyReportJob(ShopDb db) : IJob{    public async Task Execute(IJobExecutionContext context)    {        // build and send the report, honouring context.CancellationToken    }}

მიაქციე ყურადღება cron გამოსახულებას. Quartz-ის cron-ს პირველად წამების ველი აქვს და დღის ერთ-ერთ ველში ?-ს მოითხოვს, ამიტომ 0 0 3 * * ? ნიშნავს "03:00:00 ყოველდღე" - და არა ხუთველიან Unix სინტაქსს, რომელიც ადამიანების უმეტესობამ იცის. Cron გამოსახულებები ახსნილი Unix ფორმას განიხილავს; სანამ განრიგს ენდობი, მისი ვარიანტისთვის Quartz-ის საკუთარი დოკუმენტაცია წაიკითხე.

[DisallowConcurrentExecution] ნელ გაშვებას შემდეგთან გადაფარვას არ აძლევს. WaitForJobsToComplete გამორთვას მიმდინარე job-ების დასრულებას ალოდინებს, ჰოსტის გამორთვის timeout-ის ფარგლებში. განრიგები სერვერის დროის სარტყელში ფასდება, თუ trigger-ზე სხვას არ დააყენებ InTimeZone-ით, რასაც წელიწადში ორჯერ აქვს მნიშვნელობა, როცა საათები გადაიწევს.

ნაგულისხმევად Quartz განრიგს მეხსიერებაში ინახავს, ამიტომ გაშვება, რომელიც უნდა მომხდარიყო, სანამ აპლიკაცია გათიშული იყო, უბრალოდ გამოტოვებულია. თუ გამოტოვებულ გაშვებებს მნიშვნელობა აქვს, Quartz მონაცემთა ბაზის job store-ს misfire-ების დამუშავებით უჭერს მხარს; ამ ეტაპზე შეადარე ის Hangfire-ს.

Hangfire მდგრადი job-ებისთვის#

Hangfire job-ებს მონაცემთა ბაზაში ინახავს, ამიტომ ისინი restart-ებს გადაურჩებიან, ჩავარდნისას ავტომატურად ცდიან თავიდან და მათი დათვალიერება და ხელახლა გაშვება ვებ dashboard-იდან შეიძლება:

csharp
builder.Services.AddHangfire(c => c.UseSqlServerStorage(    builder.Configuration.GetConnectionString("Hangfire")));builder.Services.AddHangfireServer();var app = builder.Build();app.UseHangfireDashboard("/jobs");// enqueue from anywhere, via IBackgroundJobClientjobs.Enqueue<IWelcomeMailer>(m => m.SendAsync(userId, CancellationToken.None));// recurring, via IRecurringJobManagerrecurring.AddOrUpdate<NightlyReport>("nightly-report", r => r.RunAsync(), Cron.Daily());

როგორ მუშაობს: Enqueue მეთოდის გამოძახებას და მის არგუმენტებს საცავში ასერიალიზებს, სერვერის კომპონენტი კი საცავს polling-ით ამოწმებს და მათ უშვებს. შედეგები:

  • არგუმენტები პატარა და სერიალიზებადი უნდა იყოს. გადაეცი ID და არა entity; job გაშვებისას ახალ მონაცემებს ტვირთავს.
  • ჩავარდნილი job-ები ავტომატურად ცდიან თავიდან, ნაგულისხმევად ათჯერ, მზარდი დაყოვნებებით. სწორედ ამიტომ უნდა იყოს job-ები idempotent.
  • საცავი რეალური დამოკიდებულებაა. SQL Server საცავს Hangfire-ის ავტორები ინახავენ; PostgreSQL და MySQL საცავი საზოგადოების პაკეტებიდან მოდის; Redis საცავი კომერციული Pro გამოცემის ნაწილია. არჩევამდე შეამოწმე პაკეტი შენი მონაცემთა ბაზისთვის.
  • dashboard ნაგულისხმევად მხოლოდ ლოკალურია. მოთხოვნები ყველგანიდან, გარდა localhost-ისა, უარყოფილია, სანამ ავტორიზაციის ფილტრს არ დაამატებ. ნუ "გაასწორებ" ამას ყველასთვის დაშვებით; dashboard-ს job-ების წაშლა და ხელახლა გაშვება შეუძლია.

Hangfire-ის ბირთვი ღია კოდისაა LGPL ლიცენზიით; batch-ები და ზოგიერთი სხვა ფუნქცია ფასიან Pro გამოცემაშია.

არჩევანი და სად მუშაობს სამუშაო#

საჭიროებაგამოიყენე
ყოველ N წუთში, restart-ზე ერთი გაშვების დაკარგვა მისაღებიაBackgroundService + PeriodicTimer
fire-and-forget მოთხოვნიდან, დაკარგვა მისაღებიაChannel + BackgroundService
დადგენილ დროს, კალენდრის სტილითQuartz.NET
უნდა გადაურჩეს restart-ებს, ხელახალი ცდები, ხილვადობაHangfire, ან რიგი Valkey-ზე

სად მუშაობს სამუშაო - ეს მეორე გადაწყვეტილებაა. ერთი სერვერი ერთ პროცესს უშვებს, ამიტომ ერთ აპლიკაციის გეგმაზე ფონური სამუშაო ვებ აპლიკაციის შიგნით ცხოვრობს hosted სერვისებად - რაც აპლიკაციების უმეტესობისთვის კარგია. გაყავი ის ცალკე worker პროცესად, საკუთარ სერვერზე, როცა ფონური სამუშაო მოთხოვნებს CPU-სა ან მეხსიერებისთვის ეჯიბრება: RE:NODE-ზე CPU ნაყიდ წილზე მკაცრად შეზღუდულია, ამიტომ მძიმე ღამის job ვებ პროცესის შიგნით საიტს ანელებს, სანამ მუშაობს. Hangfire და მონაცემთა ბაზაზე დაფუძნებული რიგები ამ გაყოფას მოგვიანებით ამარტივებს, რადგან ვებ აპლიკაცია და worker მხოლოდ მონაცემთა ბაზას იზიარებენ. აპლიკაციის გეგმებში ორი database slot შედის, რაც აპლიკაციის მონაცემებისა და job store-ისთვის საკმარისია.

განხილული მაგალითი: პატარა მაღაზია ერთ აპლიკაციის გეგმაზე. მიტოვებული კალათები ყოველ საათში სუფთავდება BackgroundService-ით PeriodicTimer-ით - restart-ის გამო ერთი გაშვების დაკარგვა არაფერი ჯდება. შეკვეთის დადასტურების ელფოსტები Hangfire-ით მიდის SQL Server-ის ან PostgreSQL-ის საცავით, რადგან დაკარგული დადასტურება support ticket-ია, და Hangfire მას თავიდან ცდის, თუ mail relay ცოტა ხნით გათიშულია. ღამის გაყიდვების ანგარიში Hangfire-ის განმეორებადი job-ია და არა ცალკე Quartz-ის კონფიგურაცია, რადგან Hangfire უკვე იქ არის და ერთ scheduler-ზე ფიქრი ორზე ადვილია. ყველაფერი ვებ პროცესში მუშაობს. თუ ანგარიში ოდესმე იმდენად დამძიმდება, რომ 03:00-ზე საიტი შეანელოს, Hangfire სერვერი უცვლელად გადადის მეორე გეგმაზე, ვებ აპლიკაცია კი უბრალოდ წყვეტს AddHangfireServer()-ის გამოძახებას.

პანელის საკუთარი Schedules ჩანართი სხვა ინსტრუმენტია: ის cron გამოსახულებით დალაგებულ ამოცანებს უშვებს - კონსოლის ბრძანებას, backup-ს ან ჩართვა-გამორთვის მოქმედებას. ის სწორი ადგილია ღამის restart-ისთვის ან backup-ისთვის სარისკო job-ის წინ და არა აპლიკაციის ლოგიკისთვის. დაგეგმილი ამოცანები, რომლებიც ღირს სასარგებლოებს ჩამოთვლის.

FAQ#

ფონური job-ები ვებ აპლიკაციაში უნდა მუშაობდეს თუ ცალკე პროცესში?

ვებ აპლიკაციაში, სანამ ისინი მას CPU-სა ან მეხსიერებისთვის არ ეჯიბრებიან. ერთი პროცესის deploy და მონიტორინგი უფრო მარტივია. გაყავი, როცა job-ის დატვირთვა მოთხოვნებს შესამჩნევად ანელებს, და გამოიყენე მდგრადი job-ები, რომ ნებისმიერი პროცესის დამოუკიდებლად რესტარტი შეიძლებოდეს.

რატომ წყვეტს ჩემი BackgroundService მუშაობას ყოველგვარი შეცდომის გარეშე?

ან ერთხელ ისროლა გამონაკლისი, როცა BackgroundServiceExceptionBehavior იყო Ignore-ზე დაყენებული, ან ციკლი ნორმალურად დასრულდა - ხშირად while პირობის გამო, რომელიც false გახდა. ჩაწერე ლოგში ExecuteAsync-ის დასაწყისსა და ბოლოს, რომ დაინახო, როდის ჩერდება.

შემიძლია DbContext-ის inject-ი BackgroundService-ში?

პირდაპირ არა; ის scoped-ია, სერვისი კი singleton. inject-ე IServiceScopeFactory და შექმენი scope სამუშაოს ყოველ ერთეულზე.

როგორ გავუშვა job ზუსტად ერთხელ რამდენიმე ინსტანციაზე?

პროცესის შიდა ტაიმერი თითო ინსტანციაზე ერთხელ ეშვება. გამოიყენე Hangfire ან Quartz საერთო მონაცემთა ბაზის store-ით, რომლებიც ორივე სერვერებს შორის კოორდინაციას ახდენს, ან გაშვებამდე მონაცემთა ბაზაში lock აიღე.

Task.Run controller-ში background job-ია?

არა. ის thread pool-ზე ეშვება გამორთვის დამუშავების, შეცდომების რეპორტინგისა და scope-ის გარეშე, და restart-ზე იკარგება. ამის ნაცვლად სამუშაო ჩადე რიგში, რომელსაც hosted სერვისი ამუშავებს. .NET-ის მეხსიერება და garbage collection ხსნის, რატომ ინახავს მეხსიერებას მოსალოდნელზე დიდხანს სამუშაო, რომელიც მოთხოვნის ობიექტებს იჭერს.


კომენტარები

სრულიად ანონიმურად: ანგარიშის, ელფოსტის და cookie-ის გარეშე. ინახება მხოლოდ სახელი, ტექსტი და დრო - სხვა არაფერი. ბმულების რაოდენობა ლიმიტირებულია.

0/2000