tgstation-server 6.18.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
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(),
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
223 }
224 }
225
227 public async Task StopAsync(CancellationToken cancellationToken)
228 {
230 {
231 logger.LogDebug("Stopping instance...");
232 await ScheduleAutoUpdate(0, null);
234 await Task.WhenAll(
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
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 .AsQueryable()
393 .Where(x => x.InstanceId == metadata.Id)
394 .FirstAsync(cancellationToken);
395
396 const int ProgressSections = 7;
398 {
400 }
401
403 if (repo == null)
404 {
405 logger.LogTrace("Aborting repo update, no repository!");
406 return;
407 }
408
409 var startSha = repo.Head;
410 if (!repo.Tracking)
411 {
412 logger.LogTrace("Aborting repo update, active ref not tracking any remote branch!");
413 return;
414 }
415
417
418 // the main point of auto update is to pull the remote
419 await repo.FetchOrigin(
420 NextProgressReporter("Fetch Origin"),
421 repositorySettings.AccessUser,
422 repositorySettings.AccessToken,
423 true,
425
426 var hasDbChanges = false;
428 Models.Instance? attachedInstance = null;
430 {
431 if (currentRevInfo == null)
432 {
433 logger.LogTrace("Loading revision info for commit {sha}...", startSha[..7]);
434 currentRevInfo = await databaseContext
436 .AsQueryable()
437 .Where(x => x.CommitSha == startSha && x.InstanceId == metadata.Id)
438 .Include(x => x.ActiveTestMerges!)
439 .ThenInclude(x => x.TestMerge)
440 .FirstOrDefaultAsync(cancellationToken);
441 }
442
443 if (currentRevInfo == default)
444 {
445 logger.LogInformation(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
446 onOrigin = true;
447 }
448 else if (currentRevInfo.CommitSha == currentHead)
449 {
450 logger.LogTrace("Not updating rev-info, already in DB.");
451 return;
452 }
453
454 if (attachedInstance == null)
455 {
457 {
458 Id = metadata.Id,
459 };
460 databaseContext.Instances.Attach(attachedInstance);
461 }
462
465 {
466 CommitSha = currentHead,
467 Timestamp = await repo.TimestampCommit(currentHead, cancellationToken),
468 OriginCommitSha = onOrigin
470 : await repo.GetOriginSha(cancellationToken),
472 };
473
474 if (!onOrigin)
475 {
476 var testMerges = updatedTestMerges ?? oldRevInfo!.ActiveTestMerges!.Select(x => x.TestMerge);
479 .ToList();
480
482 }
483
484 databaseContext.RevisionInformations.Add(currentRevInfo);
485 hasDbChanges = true;
486 }
487
488 // build current commit data if it's missing
489 await UpdateRevInfo(repo.Head, false, null);
490
491 var preserveTestMerges = repositorySettings.AutoUpdatesKeepTestMerges!.Value;
493 metadata,
494 repo.RemoteGitProvider!.Value);
495
497 repo,
501
502 var result = await repo.MergeOrigin(
503 NextProgressReporter("Merge Origin"),
504 repositorySettings.CommitterName!,
505 repositorySettings.CommitterEmail!,
506 true,
508
509 // take appropriate auto update actions
510 var shouldSyncTracked = false;
511 if (result.HasValue)
512 {
513 if (updatedTestMerges.Count == 0)
514 {
515 logger.LogTrace("All test merges have been merged on remote");
516 preserveTestMerges = false;
517 }
518 else
519 {
521 currentRevInfo == default
522 || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
524
525 var currentHead = repo.Head;
526 if (currentHead != startSha)
527 {
530 }
531 }
532 }
533 else if (preserveTestMerges)
534 {
535 Chat.QueueRawDeploymentMessage("Automatic update has failed due to a conflicting testmerge!");
536 throw new JobException(Api.Models.ErrorCode.InstanceUpdateTestMergeConflict);
537 }
538
540 {
541 const string StageName = "Resetting to origin...";
542 logger.LogTrace(StageName);
543 await repo.ResetToOrigin(
544 NextProgressReporter(StageName),
545 repositorySettings.AccessUser,
546 repositorySettings.AccessToken,
547 repositorySettings.UpdateSubmodules!.Value,
548 true,
550
551 var currentHead = repo.Head;
552
554 .AsQueryable()
555 .Where(x => x.CommitSha == currentHead && x.InstanceId == metadata.Id)
556 .FirstOrDefaultAsync(cancellationToken);
557
558 if (currentHead != startSha && currentRevInfo == default)
559 await UpdateRevInfo(currentHead, true, null);
560
561 shouldSyncTracked = true;
562 }
563
564 // synch if necessary
565 if (repositorySettings.AutoUpdatesSynchronize!.Value && startSha != repo.Head && (shouldSyncTracked || repositorySettings.PushTestMergeCommits!.Value))
566 {
567 var pushedOrigin = await repo.Synchronize(
568 NextProgressReporter("Synchronize"),
569 repositorySettings.AccessUser,
570 repositorySettings.AccessToken,
571 repositorySettings.CommitterName!,
572 repositorySettings.CommitterEmail!,
574 true,
576 var currentHead = repo.Head;
577 if (currentHead != currentRevInfo!.CommitSha)
579 }
580
581 if (hasDbChanges)
582 try
583 {
584 await databaseContext.Save(cancellationToken);
585 }
586 catch
587 {
588 // DCT: Cancellation token is for job, operation must run regardless
589 await repo.ResetToSha(startSha, progressReporter, CancellationToken.None);
590 throw;
591 }
592 });
593#pragma warning restore CA1502 // Cyclomatic complexity
594
605 {
606 logger.LogDebug("Entering auto-update loop");
607 while (true)
608 try
609 {
610 TimeSpan delay;
611 if (!String.IsNullOrWhiteSpace(cron))
612 {
613 logger.LogTrace("Using cron schedule: {cron}", cron);
615 cron,
616 new CrontabSchedule.ParseOptions
617 {
618 IncludingSeconds = true,
619 });
620 var now = DateTime.UtcNow;
621 var nextOccurrence = schedule.GetNextOccurrence(now);
623 }
624 else
625 {
626 logger.LogTrace("Using interval: {interval}m", minutes);
627
628 delay = TimeSpan.FromMinutes(minutes);
629 }
630
631 logger.LogInformation("Next {desc} will occur at {time}", description, DateTimeOffset.UtcNow + delay);
632
634
636 }
638 {
639 logger.LogDebug("Cancelled {desc} loop!", description);
640 break;
641 }
642 catch (Exception e)
643 {
644 logger.LogError(e, "Error in {desc} loop!", description);
645 continue;
646 }
647
648 logger.LogTrace("Leaving {desc} loop...", description);
649 }
650
657 {
658 logger.LogInformation("Beginning auto update...");
659 await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, Enumerable.Empty<string>(), true, cancellationToken);
660
661 var repositoryUpdateJob = Job.Create(Api.Models.JobCode.RepositoryAutoUpdate, null, metadata, RepositoryRights.CancelPendingChanges);
666
668 if (repoUpdateJobResult == false)
669 {
670 logger.LogWarning("Aborting auto-update due to repository update error!");
671 return;
672 }
673
676 {
677 if (repo == null)
678 throw new JobException(Api.Models.ErrorCode.RepoMissing);
679
680 var deploySha = repo.Head;
681 if (deploySha == null)
682 {
683 logger.LogTrace("Aborting auto update, repository error!");
684 return;
685 }
686
688 {
689 logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
690 return;
691 }
692
693 // finally set up the job
694 compileProcessJob = Job.Create(Api.Models.JobCode.AutomaticDeployment, null, metadata, DreamMakerRights.CancelCompile);
697 (core, databaseContextFactory, job, progressReporter, jobCancellationToken) =>
698 {
699 if (core != this)
702 job,
703 databaseContextFactory,
706 },
708 }
709
711 }
712 }
713}
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:656
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:604
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, ILogger< Repository > logger, GeneralConfiguration generalConfiguration, 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