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:
+5
-2
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user