0

Ich bin kurz vor dem Ende eines Projekts, für das ich versuche, DDD zu verwenden, habe aber einen eklatanten Fehler entdeckt, von dem ich nicht weiß, wie ich ihn leicht lösen kann.Refactoring-Entity-Methode zur Vermeidung von Nebenläufigkeitsproblemen

Hier ist mein Wesen - ich es der Einfachheit halber reduziert haben:

public class Contribution : Entity 
{ 
    protected Contribution() 
    { 
     this.Parts = new List<ContributionPart>(); 
    } 

    internal Contribution(Guid id) 
    { 
     this.Id = id; 
     this.Parts = new List<ContributionPart>(); 
    } 

    public Guid Id { get; private set; } 

    protected virtual IList<ContributionPart> Parts { get; private set; } 

    public void UploadParts(string path, IEnumerable<long> partLengths) 
    { 
     if (this.Parts.Count > 0) 
     { 
      throw new InvalidOperationException("Parts have already been uploaded."); 
     } 

     long startPosition = 0; 
     int partNumber = 1; 

     foreach (long partLength in partLengths) 
     { 
      this.Parts.Add(new ContributionPart(this.Id, partNumber, partLength)); 
      this.Commands.Add(new UploadContributionPartCommand(this.Id, partNumber, path, startPosition, partLength)); 
      startPosition += partLength; 
      partNumber++; 
     } 
    } 

    public void SetUploadResult(int partNumber, string etag) 
    { 
     if (etag == null) 
     { 
      throw new ArgumentNullException(nameof(etag)); 
     } 

     ContributionPart part = this.Parts.SingleOrDefault(p => p.PartNumber == partNumber); 

     if (part == null) 
     { 
      throw new ContributionPartNotFoundException(this.Id, partNumber); 
     } 

     part.SetUploadResult(etag); 

     if (this.Parts.All(p => p.IsUploaded)) 
     { 
      IEnumerable<PartUploadedResult> results = this.Parts.Select(p => new PartUploadedResult(p.PartNumber, p.ETag)); 
      this.Events.Add(new ContributionUploaded(this.Id, results)); 
     } 
    } 
} 

Mein Fehler in der SetUploadResult Verfahren auftritt. Grundsätzlich führen mehrere Threads gleichzeitig Uploads durch und rufen dann am Ende des Uploads SetUploadResult auf. Da die Entität jedoch einige Sekunden zuvor geladen wurde, ruft jeder Thread SetUploadResult für eine andere Instanz der Entität auf, sodass der Test if (this.Parts.All(p => p.IsUploaded) niemals als wahr ausgewertet wird.

Ich bin mir nicht sicher, wie man das leicht löst. Die Idee hinter dem Hinzufügen von mehreren UploadContributionPartCommands zur Commands-Sammlung war, dass jedes ContributionPart parallel hochgeladen werden kann - mein CommandBus stellt dies sicher - aber mit jedem parallel hochgeladenen Teil verursacht es Probleme für meine Entitätslogik.

+0

Sie sagen also, dass mehrere Threads auf derselben Instanz einer Contribution-Entität arbeiten? – mm8

+0

Korrekt. Die Beitragseinheit hat einen UploadContributionPartCommand für jede partLength erstellt, und jeder UploadContributionPartCommandHandler wird parallel ausgeführt und führt daher Aufrufe an SetUploadResult parallel durch. Außer es ist nicht dieselbe In-Memory-Instanz der Entität, aber es ist die gleiche Entität. –

+1

Wie kommt es, dass es nicht die gleiche "In-Memory-Instanz" ist, da jede Instanz ihre eigenen Teile hat? – mm8

Antwort

0

Wenn mehrere Threads die Methode SetUploadResult gleichzeitig aufrufen und Sie eine Racebedingung haben, sollten Sie den kritischen Abschnitt mit einem Synchronisationsmechanismus wie einer Sperre schützen: https://msdn.microsoft.com/en-us/library/c5kehkcz.aspx.

Wenn Sie das Sperrfeld static machen es über alle Instanzen Ihres Entitätstyp geteilt werden, z.B .:

private static readonly object _lock = new object(); 
public void SetUploadResult(int partNumber, string etag) 
{ 
    if (etag == null) 
    { 
     throw new ArgumentNullException(nameof(etag)); 
    } 

    ContributionPart part = this.Parts.SingleOrDefault(p => p.PartNumber == partNumber); 

    if (part == null) 
    { 
     throw new ContributionPartNotFoundException(this.Id, partNumber); 
    } 

    part.SetUploadResult(etag); 

    lock (_lock) //Only one thread at a time can enter this critical section. 
       //The second thread will wait here until the first thread leaves the critical section. 
    { 
     if (this.Parts.All(p => p.IsUploaded)) 
     { 
      IEnumerable<PartUploadedResult> results = this.Parts.Select(p => new PartUploadedResult(p.PartNumber, p.ETag)); 
      this.Events.Add(new ContributionUploaded(this.Id, results)); 
     } 
    } 
} 
1

Ich glaube, Sie die Contribution so umgestalten kann, dass es nicht die SetUploadResult behandeln. Es wird die Beitragseinheit entkoppeln und die Nebenwirkungen der SetUploadResult sind isoliert, wobei die technischen Bedenken aus dem Contribution Domänenmodell herausgehalten werden.

Erstellen Sie eine Dispatcher-Klasse, die enthält, was die SetUploadResult tut.

Sobald die Entität Contribution ihre Logik ausgeführt hat, kehrt der Thread der Ausführung zum Anwendungsdienst zurück. Zu diesem Zeitpunkt können die Ereignisse von der Entität in den Dispatcher eingespeist werden.

Wenn sie lange ausgeführt werden, können Sie sie als Sammlung von Aufgaben hinzufügen und asynchron ausführen. Dann können Sie nur abwarten, wenn alle Aufgaben erledigt sind. Sie können in SO nachlesen, wie Sie dies tun können.

var results = await Task.WhenAll(task1, task2,...taskN); 
+1

Zustimmen. Das Tracking des Upload-Fortschritts ist auf jeden Fall ein Problem auf Anwendungsebene, nicht auf Domain-Ebene. – guillaume31

+0

Angemessener Kommentar. Wir hatten so viele Diskussionen über Beiträge und die Teile des Beitrags, dass wir uns entschieden haben, sie Teil des Bereichs zu machen, aber es ist vernünftig zu hinterfragen, ob sie sein sollten. –

Verwandte Themen