tgstation-server 6.19.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
Instance.cs
Go to the documentation of this file.
1using System;
3using System.Linq;
6
9
10using NCrontab;
11
12using Serilog.Context;
13
26
28{
30#pragma warning disable CA1506 // TODO: Decomplexify
31 sealed class Instance : IInstance
32 {
36 public const string DifferentCoreExceptionMessage = "Job started on different instance core!";
37
40
43
45 public IWatchdog Watchdog { get; }
46
48 public IChatManager Chat { get; }
49
52
54 public IDreamMaker DreamMaker { get; }
55
60
65
70
75
80
85
90
95
100
105
110
115
120
125
142 public Instance(
143 Api.Models.Instance metadata,
144 IRepositoryManager repositoryManager,
145 IEngineManager engineManager,
147 IWatchdog watchdog,
148 IChatManager chat,
149 StaticFiles.IConfiguration
150 configuration,
157 {
159 RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
160 EngineManager = engineManager ?? throw new ArgumentNullException(nameof(engineManager));
162 Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
163 Chat = chat ?? throw new ArgumentNullException(nameof(chat));
164 Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
171
172 timerLock = new object();
173 }
174
177 {
179 {
180 var chatDispose = Chat.DisposeAsync();
181 var watchdogDispose = Watchdog.DisposeAsync();
182 autoUpdateCts?.Dispose();
183 autoStartCts?.Dispose();
184 autoStopCts?.Dispose();
185 Configuration.Dispose();
186 dmbFactory.Dispose();
191 }
192 }
193
195 public ValueTask InstanceRenamed(string newName, CancellationToken cancellationToken)
196 {
197 ArgumentNullException.ThrowIfNull(newName);
198 if (String.IsNullOrWhiteSpace(newName))
199 throw new ArgumentException("newName cannot be whitespace!", nameof(newName));
200
202 return Watchdog.InstanceRenamed(newName, cancellationToken);
203 }
204
206 public async Task StartAsync(CancellationToken cancellationToken)
207 {
209 {
210 await Task.WhenAll(
211 ScheduleAutoUpdate(metadata.Require(x => x.AutoUpdateInterval), metadata.AutoUpdateCron).AsTask(),
212 ScheduleServerStart(null).AsTask(),
213 ScheduleServerStop(null).AsTask(),
214 Configuration.StartAsync(cancellationToken),
215 EngineManager.StartAsync(cancellationToken),
216 Chat.StartAsync(cancellationToken),
217 dmbFactory.StartAsync(cancellationToken));
218
219 // dependent on so many things, its just safer this way
220 await Watchdog.StartAsync(cancellationToken);
221
222 await dmbFactory.CleanUnusedCompileJobs(cancellationToken);
223 }
224 }
225
227 public async Task StopAsync(CancellationToken cancellationToken)
228 {
230 {
231 logger.LogDebug("Stopping instance...");
232 await ScheduleAutoUpdate(0, null);
233 await Watchdog.StopAsync(cancellationToken);
234 await Task.WhenAll(
235 Configuration.StopAsync(cancellationToken),
236 EngineManager.StopAsync(cancellationToken),
237 Chat.StopAsync(cancellationToken),
238 dmbFactory.StopAsync(cancellationToken));
239 }
240 }
241
244 {
245 if (newInterval > 0 && !String.IsNullOrWhiteSpace(newCron))
246 throw new ArgumentException("Only one of newInterval and newCron may be set!");
247
248 Task toWait;
250 if (autoUpdateTask != null)
251 {
252 logger.LogTrace("Cancelling auto-update task");
253 autoUpdateCts!.Cancel();
254 autoUpdateCts.Dispose();
256 autoUpdateTask = null;
257 autoUpdateCts = null;
258 }
259 else
260 toWait = Task.CompletedTask;
261
263 if (newInterval == 0 && String.IsNullOrWhiteSpace(newCron))
264 {
265 logger.LogTrace("Auto-update disabled 0. Not starting task.");
266 return;
267 }
268
270 {
271 // race condition, just quit
272 if (autoUpdateTask != null)
273 {
274 logger.LogWarning("Aborting auto-update scheduling change due to race condition!");
275 return;
276 }
277
280 }
281 }
282
285 {
286 Task toWait;
288 if (autoStartTask != null)
289 {
290 logger.LogTrace("Cancelling auto-start task");
291 autoStartCts!.Cancel();
292 autoStartCts.Dispose();
294 autoStartTask = null;
295 autoStartCts = null;
296 }
297 else
298 toWait = Task.CompletedTask;
299
301 if (String.IsNullOrWhiteSpace(newCron))
302 {
303 logger.LogTrace("Auto-start disabled. Not starting task.");
304 return;
305 }
306
308 {
309 // race condition, just quit
310 if (autoStartTask != null)
311 {
312 logger.LogWarning("Aborting auto-start scheduling change due to race condition!");
313 return;
314 }
315
317 autoStartTask = TimerLoop(Watchdog.Launch, "auto-start", 0, newCron, autoStartCts.Token);
318 }
319 }
320
323 {
324 Task toWait;
326 if (autoStopTask != null)
327 {
328 logger.LogTrace("Cancelling auto-stop task");
329 autoStopCts!.Cancel();
330 autoStopCts.Dispose();
332 autoStopTask = null;
333 autoStopCts = null;
334 }
335 else
336 toWait = Task.CompletedTask;
337
339 if (String.IsNullOrWhiteSpace(newCron))
340 {
341 logger.LogTrace("Auto-stop disabled. Not stoping task.");
342 return;
343 }
344
346 {
347 // race condition, just quit
348 if (autoStopTask != null)
349 {
350 logger.LogWarning("Aborting auto-stop scheduling change due to race condition!");
351 return;
352 }
353
356 async cancellationToken => await Watchdog.Terminate(true, cancellationToken),
357 "auto-stop",
358 0,
359 newCron,
360 autoStopCts.Token);
361 }
362 }
363
366
376#pragma warning disable CA1502 // Cyclomatic complexity
379 IDatabaseContextFactory databaseContextFactory,
380 Job job,
382 CancellationToken cancellationToken)
383 => databaseContextFactory.UseContext(
384 async databaseContext =>
385 {
386 if (core != this)
388
389 // assume 5 steps with synchronize
390 var repositorySettingsTask = databaseContext
391 .RepositorySettings
392 .Where(x => x.InstanceId == metadata.Id)
393 .FirstAsync(cancellationToken);
394
395 const int ProgressSections = 7;
397 {
399 }
400
401 using var repo = await RepositoryManager.LoadRepository(cancellationToken);
402 if (repo == null)
403 {
404 logger.LogTrace("Aborting repo update, no repository!");
405 return;
406 }
407
408 var startSha = repo.Head;
409 if (!repo.Tracking)
410 {
411 logger.LogTrace("Aborting repo update, active ref not tracking any remote branch!");
412 return;
413 }
414
416
417 // the main point of auto update is to pull the remote
418 await repo.FetchOrigin(
419 NextProgressReporter("Fetch Origin"),
420 repositorySettings.AccessUser,
421 repositorySettings.AccessToken,
422 true,
423 cancellationToken);
424
425 var hasDbChanges = false;
427 Models.Instance? attachedInstance = null;
429 {
430 if (currentRevInfo == null)
431 {
432 logger.LogTrace("Loading revision info for commit {sha}...", startSha[..7]);
433 currentRevInfo = await databaseContext
435 .Where(x => x.CommitSha == startSha && x.InstanceId == metadata.Id)
436 .Include(x => x.ActiveTestMerges!)
437 .ThenInclude(x => x.TestMerge)
438 .FirstOrDefaultAsync(cancellationToken);
439 }
440
441 if (currentRevInfo == default)
442 {
443 logger.LogInformation(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
444 onOrigin = true;
445 }
446 else if (currentRevInfo.CommitSha == currentHead)
447 {
448 logger.LogTrace("Not updating rev-info, already in DB.");
449 return;
450 }
451
452 if (attachedInstance == null)
453 {
455 {
456 Id = metadata.Id,
457 };
458 databaseContext.Instances.Attach(attachedInstance);
459 }
460
463 {
464 CommitSha = currentHead,
465 Timestamp = await repo.TimestampCommit(currentHead, cancellationToken),
466 OriginCommitSha = onOrigin
468 : await repo.GetOriginSha(cancellationToken),
470 };
471
472 if (!onOrigin)
473 {
474 var testMerges = updatedTestMerges ?? oldRevInfo!.ActiveTestMerges!.Select(x => x.TestMerge);
477 .ToList();
478
480 }
481
482 databaseContext.RevisionInformations.Add(currentRevInfo);
483 hasDbChanges = true;
484 }
485
486 // build current commit data if it's missing
487 await UpdateRevInfo(repo.Head, false, null);
488
489 var preserveTestMerges = repositorySettings.AutoUpdatesKeepTestMerges!.Value;
491 metadata,
492 repo.RemoteGitProvider!.Value);
493
495 repo,
498 cancellationToken);
499
500 var result = await repo.MergeOrigin(
501 NextProgressReporter("Merge Origin"),
502 repositorySettings.CommitterName!,
503 repositorySettings.CommitterEmail!,
504 true,
505 cancellationToken);
506
507 // take appropriate auto update actions
508 var shouldSyncTracked = false;
509 if (result.HasValue)
510 {
511 if (updatedTestMerges.Count == 0)
512 {
513 logger.LogTrace("All test merges have been merged on remote");
514 preserveTestMerges = false;
515 }
516 else
517 {
519 currentRevInfo == default
520 || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
522
523 var currentHead = repo.Head;
524 if (currentHead != startSha)
525 {
528 }
529 }
530 }
531 else if (preserveTestMerges)
532 {
533 Chat.QueueRawDeploymentMessage("Automatic update has failed due to a conflicting testmerge!");
534 throw new JobException(Api.Models.ErrorCode.InstanceUpdateTestMergeConflict);
535 }
536
538 {
539 const string StageName = "Resetting to origin...";
540 logger.LogTrace(StageName);
541 await repo.ResetToOrigin(
542 NextProgressReporter(StageName),
543 repositorySettings.AccessUser,
544 repositorySettings.AccessToken,
545 repositorySettings.UpdateSubmodules!.Value,
546 true,
547 cancellationToken);
548
549 var currentHead = repo.Head;
550
551 currentRevInfo = await databaseContext
552 .RevisionInformations
553 .Where(x => x.CommitSha == currentHead && x.InstanceId == metadata.Id)
554 .FirstOrDefaultAsync(cancellationToken);
555
556 if (currentHead != startSha && currentRevInfo == default)
557 await UpdateRevInfo(currentHead, true, null);
558
559 shouldSyncTracked = true;
560 }
561
562 // synch if necessary
563 if (repositorySettings.AutoUpdatesSynchronize!.Value && startSha != repo.Head && (shouldSyncTracked || repositorySettings.PushTestMergeCommits!.Value))
564 {
565 var pushedOrigin = await repo.Synchronize(
566 NextProgressReporter("Synchronize"),
567 repositorySettings.AccessUser,
568 repositorySettings.AccessToken,
569 repositorySettings.CommitterName!,
570 repositorySettings.CommitterEmail!,
572 true,
573 cancellationToken);
574 var currentHead = repo.Head;
575 if (currentHead != currentRevInfo!.CommitSha)
577 }
578
579 if (hasDbChanges)
580 try
581 {
582 await databaseContext.Save(cancellationToken);
583 }
584 catch
585 {
586 // DCT: Cancellation token is for job, operation must run regardless
588 throw;
589 }
590 });
591#pragma warning restore CA1502 // Cyclomatic complexity
592
603 {
604 logger.LogDebug("Entering auto-update loop");
605 while (true)
606 try
607 {
608 TimeSpan delay;
609 if (!String.IsNullOrWhiteSpace(cron))
610 {
611 logger.LogTrace("Using cron schedule: {cron}", cron);
613 cron,
614 new CrontabSchedule.ParseOptions
615 {
616 IncludingSeconds = true,
617 });
618 var now = DateTime.UtcNow;
619 var nextOccurrence = schedule.GetNextOccurrence(now);
621 }
622 else
623 {
624 logger.LogTrace("Using interval: {interval}m", minutes);
625
626 delay = TimeSpan.FromMinutes(minutes);
627 }
628
629 logger.LogInformation("Next {desc} will occur at {time}", description, DateTimeOffset.UtcNow + delay);
630
631 await asyncDelayer.Delay(delay, cancellationToken);
632
633 await timerAction(cancellationToken);
634 }
636 {
637 logger.LogDebug("Cancelled {desc} loop!", description);
638 break;
639 }
640 catch (Exception e)
641 {
642 logger.LogError(e, "Error in {desc} loop!", description);
643 continue;
644 }
645
646 logger.LogTrace("Leaving {desc} loop...", description);
647 }
648
655 {
656 logger.LogInformation("Beginning auto update...");
657 await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, Enumerable.Empty<string>(), true, cancellationToken);
658
659 var repositoryUpdateJob = Job.Create(Api.Models.JobCode.RepositoryAutoUpdate, null, metadata, RepositoryRights.CancelPendingChanges);
663 cancellationToken);
664
665 var repoUpdateJobResult = await jobManager.WaitForJobCompletion(repositoryUpdateJob, null, cancellationToken, cancellationToken);
666 if (repoUpdateJobResult == false)
667 {
668 logger.LogWarning("Aborting auto-update due to repository update error!");
669 return;
670 }
671
673 using (var repo = await RepositoryManager.LoadRepository(cancellationToken))
674 {
675 if (repo == null)
676 throw new JobException(Api.Models.ErrorCode.RepoMissing);
677
678 var deploySha = repo.Head;
679 if (deploySha == null)
680 {
681 logger.LogTrace("Aborting auto update, repository error!");
682 return;
683 }
684
686 {
687 logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
688 return;
689 }
690
691 // finally set up the job
692 compileProcessJob = Job.Create(Api.Models.JobCode.AutomaticDeployment, null, metadata, DreamMakerRights.CancelCompile);
695 (core, databaseContextFactory, job, progressReporter, jobCancellationToken) =>
696 {
697 if (core != this)
700 job,
701 databaseContextFactory,
704 },
705 cancellationToken);
706 }
707
708 await jobManager.WaitForJobCompletion(compileProcessJob, null, default, cancellationToken);
709 }
710 }
711}
Metadata about a server instance.
Definition Instance.cs:9
async ValueTask DeploymentProcess(Models.Job job, IDatabaseContextFactory databaseContextFactory, JobProgressReporter progressReporter, CancellationToken cancellationToken)
Create and a compile job and insert it into the database. Meant to be called by a IJobManager....
Task StopAsync(CancellationToken cancellationToken)
async Task StartAsync(CancellationToken cancellationToken)
ValueTask< CompileJob?> LatestCompileJob()
Gets the latest CompileJob.A ValueTask<TResult> resulting in the latest CompileJob or null if none ar...
async Task StartAsync(CancellationToken cancellationToken)
Definition Instance.cs:206
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IEngineManager engineManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChatManager chat, StaticFiles.IConfiguration configuration, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, IAsyncDelayer asyncDelayer, ILogger< Instance > logger)
Initializes a new instance of the Instance class.
Definition Instance.cs:142
readonly IEventConsumer eventConsumer
The IEventConsumer for the Instance.
Definition Instance.cs:69
Task? autoStartTask
The auto-start Task.
Definition Instance.cs:109
async ValueTask ScheduleServerStart(string? newCron)
Change the server auto-start timing for the IInstanceCore.A ValueTask representing the running operat...
Definition Instance.cs:284
IChatManager Chat
The IChatManager for the IInstanceCore.
Definition Instance.cs:48
StaticFiles.IConfiguration Configuration
The IConfiguration for the IInstanceCore.
Definition Instance.cs:51
readonly IDmbFactory dmbFactory
The IDmbFactory for the Instance.
Definition Instance.cs:59
CancellationTokenSource? autoUpdateCts
CancellationTokenSource for autoUpdateTask.
Definition Instance.cs:104
async ValueTask ScheduleAutoUpdate(uint newInterval, string? newCron)
Change the auto-update timing for the IInstanceCore.A ValueTask representing the running operation.
Definition Instance.cs:243
readonly IJobManager jobManager
The IJobManager for the Instance.
Definition Instance.cs:64
CancellationTokenSource? autoStartCts
CancellationTokenSource for autoStartTask.
Definition Instance.cs:114
CancellationTokenSource? autoStopCts
CancellationTokenSource for autoStopTask.
Definition Instance.cs:124
IWatchdog Watchdog
The IWatchdog for the IInstanceCore.
Definition Instance.cs:45
async ValueTask AutoUpdateAction(CancellationToken cancellationToken)
Pulls the repository and compiles.
Definition Instance.cs:654
async ValueTask ScheduleServerStop(string? newCron)
Change the server auto-stop timing for the IInstanceCore.A ValueTask representing the running operati...
Definition Instance.cs:322
readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory
The IRemoteDeploymentManagerFactory for the Instance.
Definition Instance.cs:74
Task? autoUpdateTask
The auto-update Task.
Definition Instance.cs:99
readonly ILogger< Instance > logger
The ILogger for the Instance.
Definition Instance.cs:84
const string DifferentCoreExceptionMessage
Message for the InvalidOperationException if ever a job starts on a different IInstanceCore than the ...
Definition Instance.cs:36
readonly Api.Models.Instance metadata
The Api.Models.Instance for the Instance.
Definition Instance.cs:89
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the Instance.
Definition Instance.cs:79
async Task TimerLoop(Func< CancellationToken, ValueTask > timerAction, string description, uint minutes, string? cron, CancellationToken cancellationToken)
Runs a timerAction every set of given minutes or on a given cron schedule.
Definition Instance.cs:602
ValueTask RepositoryAutoUpdateJob(IInstanceCore? core, IDatabaseContextFactory databaseContextFactory, Job job, JobProgressReporter progressReporter, CancellationToken cancellationToken)
The JobEntrypoint for updating the repository.
readonly object timerLock
lock object for autoUpdateCts and autoUpdateTask.
Definition Instance.cs:94
ValueTask InstanceRenamed(string newName, CancellationToken cancellationToken)
Called when the owning Instance is renamed.A ValueTask representing the running operation.
Definition Instance.cs:195
async Task StopAsync(CancellationToken cancellationToken)
Definition Instance.cs:227
Task? autoStopTask
The auto-stop Task.
Definition Instance.cs:119
Repository(LibGit2Sharp.IRepository libGitRepo, ILibGit2Commands commands, IIOManager ioManager, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, IPostWriteHandler postWriteHandler, IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, ILibGit2RepositoryFactory submoduleFactory, IOptionsMonitor< GeneralConfiguration > generalConfigurationOptions, ILogger< Repository > logger, Action disposeAction)
Initializes a new instance of the Repository class.
async ValueTask< IRepository?> LoadRepository(CancellationToken cancellationToken)
Attempt to load the IRepository from the default location.A ValueTask<TResult> resulting in the loade...
Operation exceptions thrown from the context of a Models.Job.
JobProgressReporter CreateSection(string? newStageName, double percentage)
Create a subsection of the JobProgressReporter with its optional own stage name.
Represents an Api.Models.Instance in the database.
Definition Instance.cs:11
ICollection< RevisionInformation > RevisionInformations
The RevisionInformations in the Instance.
Definition Instance.cs:50
static Job Create(JobCode code, User? startedBy, Api.Models.Instance instance)
Creates a new job for registering in the Jobs.IJobService.
Many to many relationship for Models.RevisionInformation and Models.TestMerge.
Instance? Instance
The Models.Instance the RevisionInformation belongs to.
Helpers for manipulating the Serilog.Context.LogContext.
const string InstanceIdContextProperty
The Serilog.Context.LogContext property name for Models.Instance Api.Models.EntityId....
For managing connected chat services.
void QueueRawDeploymentMessage(string message)
Queue a chat message to configured deployment channels.
ValueTask CleanUnusedCompileJobs(CancellationToken cancellationToken)
Deletes all compile jobs that are inactive in the Game folder.
ValueTask< CompileJob?> LatestCompileJob()
Gets the latest CompileJob.
IRemoteDeploymentManager CreateRemoteDeploymentManager(Api.Models.Instance metadata, RemoteGitProvider remoteGitProvider)
Creates a IRemoteDeploymentManager for a given remoteGitProvider .
ValueTask< IReadOnlyCollection< TestMerge > > RemoveMergedTestMerges(IRepository repository, RepositorySettings repositorySettings, RevisionInformation revisionInformation, CancellationToken cancellationToken)
Get the updated list of TestMerges for an origin merge.
Consumes EventTypes and takes the appropriate actions.
ValueTask HandleEvent(EventType eventType, IEnumerable< string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
Handle a given eventType .
For interacting with the instance services.
Component version of IInstanceCore.
Definition IInstance.cs:9
ValueTask InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
Called when the owning Instance is renamed.
Runs and monitors the twin server controllers.
Definition IWatchdog.cs:16
ValueTask Launch(CancellationToken cancellationToken)
Start the IWatchdog.
ValueTask Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog.
Factory for scoping usage of IDatabaseContexts. Meant for use by Components.
Manages the runtime of Jobs.
ValueTask< bool?> WaitForJobCompletion(Job job, User? canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
Wait for a given job to complete.
ValueTask RegisterOperation(Job job, JobEntrypoint operation, CancellationToken cancellationToken)
Registers a given Job and begins running it.
ValueTask Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan .
@ List
User may list files if the Models.Instance allows it.
DreamMakerRights
Rights for deployment.
RepositoryRights
Rights for the git repository.
EventType
Types of events. Mirror in tgs.dm. Prefer last listed name for script.
Definition EventType.cs:7