Make dedupe robust: independent sides, wait for sync, report failures

Nach der Vorschau blieben Dubletten teils stehen. Ursachen abgesichert:

- Outlook- und Anlagen-Seite laufen jetzt in getrennten try-Bloecken. Vorher
  brach ein Fehler beim Laden/Verarbeiten der Anlage die ganze Methode ab,
  bevor die Outlook-Seite ueberhaupt bereinigt wurde.
- RunExclusiveAsync wartet bis zu 60s auf einen laufenden Sync, statt sofort
  abzubrechen (Auto-Sync feuert jede Minute -> haeufige Kollision).
- Fehlgeschlagene Loeschungen werden jetzt als Fehler gemeldet (vorher still
  verschluckt), plus Gruppen-Zaehler ins Log.
- DedupeForm zeigt einen deutlichen Hinweis (MessageBox), wenn wegen eines
  laufenden Syncs nicht ausgefuehrt wurde, und laesst einen erneuten Versuch zu.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 14:04:49 +02:00
co-authored by Claude Opus 4.8
parent 6cc3103040
commit 0076d84062
4 changed files with 94 additions and 59 deletions
+5 -2
View File
@@ -16,8 +16,11 @@ Versionsschema ist `x.x.x.x` (siehe `release.sh`).
Felder aus den Dubletten aufgefuellt werden (kein Datenverlust), die uebrigen Felder aus den Dubletten aufgefuellt werden (kein Datenverlust), die uebrigen
werden geloescht. Verwaiste Zuordnungen werden bereinigt, sodass der naechste werden geloescht. Verwaiste Zuordnungen werden bereinigt, sodass der naechste
bidirektionale Sync die Gewinner sauber 1:1 verbindet. Laeuft unter derselben bidirektionale Sync die Gewinner sauber 1:1 verbindet. Laeuft unter derselben
Sperre wie ein Sync (kein gleichzeitiger Lauf), nutzt dieselbe Kontakt- Sperre wie ein Sync (kein gleichzeitiger Lauf - wartet bei laufendem Sync bis
Erkennung wie der Sync und wird vollstaendig protokolliert. zu 60s statt abzubrechen), nutzt dieselbe Kontakt-Erkennung wie der Sync und
wird vollstaendig protokolliert. Outlook- und Anlagen-Seite werden unabhaengig
verarbeitet (eine streikende Anlage verhindert nicht die Outlook-Bereinigung);
fehlgeschlagene Loeschungen werden gemeldet.
### Behoben ### Behoben
@@ -90,90 +90,106 @@ namespace StarfaceOutlookSync.Services
var deletedStarfaceIds = new HashSet<string>(); var deletedStarfaceIds = new HashSet<string>();
var deletedOutlookIds = new HashSet<string>(); var deletedOutlookIds = new HashSet<string>();
var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath); // --- Outlook (lokal, unabhaengig - laeuft auch wenn die Anlage streikt) ---
try
using (var backend = ContactBackendFactory.Create(profile.Connection))
{ {
backend.OnDebug += Log; var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath);
if (!await backend.LoginAsync()) var groups = GroupDuplicates(outlookContacts, c => mappedOutlook.Contains(c.OutlookEntryId));
throw new Exception("Login an der Telefonanlage fehlgeschlagen."); Log($"Outlook: {groups.Count} Gruppe(n) mit Dubletten.");
foreach (var g in groups)
var remoteContacts = await backend.GetContactsAsync(profile.AddressBook);
// --- Telefonanlage ---
foreach (var g in GroupDuplicates(remoteContacts, c => mappedStarface.Contains(c.StarfaceId)))
{ {
try try
{ {
bool changed = ContactMerger.FillEmptyInto(g.Survivor, g.Duplicates); bool changed = ContactMerger.FillEmptyInto(g.Survivor, g.Duplicates);
bool survivorOk = true; if (changed && _outlookService.UpdateContact(g.Survivor.OutlookEntryId, g.Survivor) == null)
if (changed)
survivorOk = await backend.UpdateContactAsync(g.Survivor.StarfaceId, g.Survivor, profile.AddressBook) != null;
if (!survivorOk)
{ {
// Gewinner konnte nicht ergaenzt werden -> Dubletten NICHT loeschen
// (sonst gingen die nur dort vorhandenen Felder verloren).
report.Errors++; report.Errors++;
report.Messages.Add($"Anlage: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - Gruppe uebersprungen."); report.Messages.Add($"Outlook: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - uebersprungen.");
continue; continue;
} }
foreach (var dup in g.Duplicates) foreach (var dup in g.Duplicates)
{ {
if (await backend.DeleteContactAsync(dup.StarfaceId)) if (_outlookService.DeleteContact(dup.OutlookEntryId))
{ {
deletedStarfaceIds.Add(dup.StarfaceId); deletedOutlookIds.Add(dup.OutlookEntryId);
report.RemoteDeleted++; report.OutlookDeleted++;
Log($" Anlage: Dublette geloescht -> zusammengefuehrt in '{g.Survivor.DisplayName}'"); Log($" Outlook: Dublette geloescht -> '{g.Survivor.DisplayName}'");
}
else
{
report.Errors++;
report.Messages.Add($"Outlook: Dublette von '{g.Survivor.DisplayName}' konnte nicht geloescht werden.");
} }
} }
report.RemoteMerged++; report.OutlookMerged++;
} }
catch (Exception ex) catch (Exception ex)
{ {
report.Errors++; report.Errors++;
report.Messages.Add($"Anlage '{g.Survivor.DisplayName}': {ex.Message}"); report.Messages.Add($"Outlook '{g.Survivor.DisplayName}': {ex.Message}");
} }
} }
}
await backend.LogoutAsync(); catch (Exception ex)
{
report.Errors++;
report.Messages.Add("Outlook gesamt: " + ex.Message);
} }
// --- Outlook --- // --- Telefonanlage (unabhaengig) ---
foreach (var g in GroupDuplicates(outlookContacts, c => mappedOutlook.Contains(c.OutlookEntryId))) try
{ {
try using (var backend = ContactBackendFactory.Create(profile.Connection))
{ {
bool changed = ContactMerger.FillEmptyInto(g.Survivor, g.Duplicates); backend.OnDebug += Log;
bool survivorOk = true; if (!await backend.LoginAsync())
if (changed) throw new Exception("Login an der Telefonanlage fehlgeschlagen.");
survivorOk = _outlookService.UpdateContact(g.Survivor.OutlookEntryId, g.Survivor) != null;
if (!survivorOk) var remoteContacts = await backend.GetContactsAsync(profile.AddressBook);
var groups = GroupDuplicates(remoteContacts, c => mappedStarface.Contains(c.StarfaceId));
Log($"Telefonanlage: {groups.Count} Gruppe(n) mit Dubletten.");
foreach (var g in groups)
{ {
report.Errors++; try
report.Messages.Add($"Outlook: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - Gruppe uebersprungen.");
continue;
}
foreach (var dup in g.Duplicates)
{
if (_outlookService.DeleteContact(dup.OutlookEntryId))
{ {
deletedOutlookIds.Add(dup.OutlookEntryId); bool changed = ContactMerger.FillEmptyInto(g.Survivor, g.Duplicates);
report.OutlookDeleted++; if (changed && await backend.UpdateContactAsync(g.Survivor.StarfaceId, g.Survivor, profile.AddressBook) == null)
Log($" Outlook: Dublette geloescht -> zusammengefuehrt in '{g.Survivor.DisplayName}'"); {
report.Errors++;
report.Messages.Add($"Anlage: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - uebersprungen.");
continue;
}
foreach (var dup in g.Duplicates)
{
if (await backend.DeleteContactAsync(dup.StarfaceId))
{
deletedStarfaceIds.Add(dup.StarfaceId);
report.RemoteDeleted++;
Log($" Anlage: Dublette geloescht -> '{g.Survivor.DisplayName}'");
}
else
{
report.Errors++;
report.Messages.Add($"Anlage: Dublette von '{g.Survivor.DisplayName}' konnte nicht geloescht werden.");
}
}
report.RemoteMerged++;
}
catch (Exception ex)
{
report.Errors++;
report.Messages.Add($"Anlage '{g.Survivor.DisplayName}': {ex.Message}");
} }
} }
report.OutlookMerged++;
} await backend.LogoutAsync();
catch (Exception ex)
{
report.Errors++;
report.Messages.Add($"Outlook '{g.Survivor.DisplayName}': {ex.Message}");
} }
} }
catch (Exception ex)
{
report.Errors++;
report.Messages.Add("Telefonanlage gesamt: " + ex.Message);
}
// Verwaiste Zuordnungen (zeigen auf geloeschte Dubletten) entfernen. // Verwaiste Zuordnungen (zeigen auf geloeschte Dubletten) entfernen.
// Der naechste Sync verbindet die Gewinner sauber neu. // Der naechste Sync verbindet die Gewinner sauber neu.
@@ -123,10 +123,18 @@ namespace StarfaceOutlookSync.Services
/// </summary> /// </summary>
public async Task<bool> RunExclusiveAsync(Func<Task> action, Action<string> status) public async Task<bool> RunExclusiveAsync(Func<Task> action, Action<string> status)
{ {
if (Interlocked.CompareExchange(ref _running, 1, 0) != 0) // Auf einen gerade laufenden Sync warten (bis 60s), statt sofort
// abzubrechen - der Auto-Sync feuert ja jede Minute.
var guardDeadline = DateTime.UtcNow.AddSeconds(60);
while (Interlocked.CompareExchange(ref _running, 1, 0) != 0)
{ {
status?.Invoke("Es laeuft bereits ein Sync / eine Aktion - bitte warten."); if (DateTime.UtcNow >= guardDeadline)
return false; {
status?.Invoke("Es laeuft bereits ein Sync - bitte spaeter erneut versuchen.");
return false;
}
status?.Invoke("Warte, bis der laufende Sync fertig ist...");
await Task.Delay(1000);
} }
SyncLock crossLock = null; SyncLock crossLock = null;
+9 -1
View File
@@ -160,7 +160,15 @@ namespace StarfaceOutlookSync.UI
if (!ran) if (!ran)
{ {
AppendLog("Abgebrochen (Sync/Aktion laeuft oder anderer Arbeitsplatz aktiv)."); AppendLog("Abgebrochen: gerade laeuft ein Sync (oder ein anderer Arbeitsplatz ist aktiv).");
_lblSummary.Text = "Nicht ausgefuehrt - es laeuft gerade ein Sync. Bitte kurz warten und erneut versuchen.";
_lblSummary.ForeColor = Color.OrangeRed;
MessageBox.Show(this,
"Die Zusammenfuehrung wurde NICHT ausgefuehrt, weil gerade ein Sync laeuft " +
"(oder ein anderer Arbeitsplatz aktiv ist).\n\nBitte kurz warten und erneut auf " +
"'Zusammenfuehren starten' klicken.",
"Dubletten", MessageBoxButtons.OK, MessageBoxIcon.Warning);
_btnStart.Enabled = true; // erneuter Versuch moeglich
} }
else if (report != null) else if (report != null)
{ {