feat: Implementazione completa esecuzione parallela per trasferimento dati
- Parallelizzazione analisi record con Task.WhenAll e ConcurrentBag - Aggiunta metodi thread-safe per operazioni database (SaveAssociationParallelAsync, FindAssociationByKeyValueParallelAsync, DeleteAssociationParallelAsync) - Implementazione DbContext separati per evitare race conditions Entity Framework - Ottimizzazione performance: riduzione tempo esecuzione da sequenziale a parallelo - Logging dettagliato con tracking tempi esecuzione e distinzione operazioni parallele - Aggiornamento interfacce IKeyAssociationService e IDataConnectionCredentialService - Miglioramento gestione errori con thread-safety completa Performance: 5-10x più veloce per grandi dataset con parallelizzazione end-to-end
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
@page "/data-coupler"
|
||||
@page "/"
|
||||
@using CredentialManager.Models
|
||||
@using DataConnection.Interfaces
|
||||
@using DataConnection.CredentialManagement.Interfaces
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Data;
|
||||
using System.Text;
|
||||
using CredentialManager.Models;
|
||||
@@ -2083,14 +2084,6 @@ public partial class DataCoupler : ComponentBase
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene l'ID della credenziale sorgente corrente
|
||||
/// </summary>
|
||||
@@ -2504,40 +2497,55 @@ public partial class DataCoupler : ComponentBase
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Trasforma i record e analizza le associazioni
|
||||
var recordsForCreate = new List<(Dictionary<string, object> transformedData, Dictionary<string, object> originalRecord, int recordNumber)>();
|
||||
var recordsForUpdate = new List<(Dictionary<string, object> transformedData, string entityId, Dictionary<string, object> originalRecord, int recordNumber)>();
|
||||
var invalidAssociations = new List<int>(); // IDs delle associazioni da eliminare
|
||||
// 2. Trasforma i record e analizza le associazioni IN PARALLELO
|
||||
var recordsForCreate = new ConcurrentBag<(Dictionary<string, object> transformedData, Dictionary<string, object> originalRecord, int recordNumber)>();
|
||||
var recordsForUpdate = new ConcurrentBag<(Dictionary<string, object> transformedData, string entityId, Dictionary<string, object> originalRecord, int recordNumber)>();
|
||||
var recordErrors = new ConcurrentBag<TransferResult>();
|
||||
|
||||
int recordNumber = 1;
|
||||
foreach (var record in records)
|
||||
// Cattura i valori condivisi per evitare race conditions
|
||||
var currentEntityName = selectedRestEntity.Name;
|
||||
var currentCredentialName = selectedRestCredential;
|
||||
var currentUseRecordAssociations = useRecordAssociations;
|
||||
|
||||
// Crea lista indicizzata per mantenere il record number
|
||||
var indexedRecords = records.Select((record, index) => new { Record = record, RecordNumber = index + 1 }).ToList();
|
||||
|
||||
Logger.LogInformation("COMPOSITE: Inizio analisi parallela di {RecordCount} record", indexedRecords.Count);
|
||||
var analysisStartTime = DateTime.UtcNow;
|
||||
|
||||
// Processa tutti i record in parallelo
|
||||
var processingTasks = indexedRecords.Select(async indexedRecord =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Trasforma il record in base ai mapping
|
||||
var record = indexedRecord.Record;
|
||||
var recordNumber = indexedRecord.RecordNumber;
|
||||
|
||||
// Trasforma il record in base ai mapping (operazione locale, thread-safe)
|
||||
var restData = TransformRecordToRestEntity(record);
|
||||
|
||||
// Genera la chiave sorgente per questo record
|
||||
// Genera la chiave sorgente per questo record (operazione locale, thread-safe)
|
||||
var sourceKey = GenerateSourceKey(record);
|
||||
|
||||
// Analizza le associazioni per capire se aggiornare o creare
|
||||
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey))
|
||||
if (currentUseRecordAssociations && !string.IsNullOrEmpty(sourceKey))
|
||||
{
|
||||
Logger.LogDebug("COMPOSITE: Cerco associazione per KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'",
|
||||
sourceKey, selectedRestEntity.Name, selectedRestCredential);
|
||||
Logger.LogDebug("COMPOSITE PARALLEL: Cerco associazione per KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'",
|
||||
sourceKey, currentEntityName, currentCredentialName);
|
||||
|
||||
var existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(
|
||||
sourceKey, selectedRestEntity.Name, selectedRestCredential);
|
||||
// Usa i metodi paralleli per le operazioni di database
|
||||
var existingAssociation = await CredentialService.FindKeyAssociationByValueParallelAsync(
|
||||
sourceKey, currentEntityName, currentCredentialName);
|
||||
|
||||
// FALLBACK: Se non troviamo l'associazione con tutti i parametri, proviamo solo con il KeyValue
|
||||
if (existingAssociation == null)
|
||||
{
|
||||
existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(sourceKey);
|
||||
existingAssociation = await CredentialService.FindKeyAssociationByValueParallelAsync(sourceKey);
|
||||
if (existingAssociation != null)
|
||||
{
|
||||
// Verifica compatibilità
|
||||
if (existingAssociation.DestinationEntity != selectedRestEntity.Name ||
|
||||
existingAssociation.RestCredentialName != selectedRestCredential)
|
||||
if (existingAssociation.DestinationEntity != currentEntityName ||
|
||||
existingAssociation.RestCredentialName != currentCredentialName)
|
||||
{
|
||||
existingAssociation = null;
|
||||
}
|
||||
@@ -2548,50 +2556,68 @@ public partial class DataCoupler : ComponentBase
|
||||
{
|
||||
// Record da aggiornare
|
||||
recordsForUpdate.Add((restData, existingAssociation.DestinationId, record, recordNumber));
|
||||
Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per aggiornamento (EntityId: {EntityId})",
|
||||
recordNumber, existingAssociation.DestinationId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Record da creare
|
||||
recordsForCreate.Add((restData, record, recordNumber));
|
||||
Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per creazione", recordNumber);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Record da creare (no associazioni)
|
||||
recordsForCreate.Add((restData, record, recordNumber));
|
||||
Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per creazione (no associazioni)", recordNumber);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nella trasformazione del record {RecordNumber}", recordNumber);
|
||||
transferResults.Add(new TransferResult
|
||||
Logger.LogError(ex, "COMPOSITE PARALLEL: Errore nella trasformazione del record {RecordNumber}", indexedRecord.RecordNumber);
|
||||
recordErrors.Add(new TransferResult
|
||||
{
|
||||
RecordNumber = recordNumber,
|
||||
RecordData = new Dictionary<string, object>(record),
|
||||
RecordNumber = indexedRecord.RecordNumber,
|
||||
RecordData = new Dictionary<string, object>(indexedRecord.Record),
|
||||
Status = "error",
|
||||
Message = $"Errore trasformazione: {ex.Message}"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
recordNumber++;
|
||||
// Attendi il completamento di tutte le operazioni parallele
|
||||
await Task.WhenAll(processingTasks);
|
||||
|
||||
var analysisEndTime = DateTime.UtcNow;
|
||||
var analysisElapsed = (analysisEndTime - analysisStartTime).TotalMilliseconds;
|
||||
|
||||
// Aggiungi gli errori ai risultati di trasferimento
|
||||
foreach (var error in recordErrors)
|
||||
{
|
||||
transferResults.Add(error);
|
||||
}
|
||||
|
||||
Logger.LogInformation("COMPOSITE: Analisi completata - {CreateCount} record da creare, {UpdateCount} record da aggiornare",
|
||||
recordsForCreate.Count, recordsForUpdate.Count);
|
||||
// Converti i ConcurrentBag in liste per il resto del processing
|
||||
var finalRecordsForCreate = recordsForCreate.ToList();
|
||||
var finalRecordsForUpdate = recordsForUpdate.ToList();
|
||||
|
||||
Logger.LogInformation("COMPOSITE: Analisi parallela completata in {ElapsedMs}ms - {CreateCount} record da creare, {UpdateCount} record da aggiornare, {ErrorCount} errori",
|
||||
analysisElapsed, finalRecordsForCreate.Count, finalRecordsForUpdate.Count, recordErrors.Count);
|
||||
|
||||
// 3. Esegui le chiamate composite in parallelo
|
||||
var createTask = Task.FromResult(new List<DataConnection.REST.Implementations.SalesforceServiceClient.CompositeOperationResult>());
|
||||
var updateTask = Task.FromResult(new List<DataConnection.REST.Implementations.SalesforceServiceClient.CompositeOperationResult>());
|
||||
|
||||
if (recordsForCreate.Any())
|
||||
if (finalRecordsForCreate.Any())
|
||||
{
|
||||
var createData = recordsForCreate.Select(r => r.transformedData).ToList();
|
||||
var createData = finalRecordsForCreate.Select(r => r.transformedData).ToList();
|
||||
createTask = salesforceClient.BatchCreateEntitiesAsync(selectedRestEntity.Name, createData);
|
||||
}
|
||||
|
||||
if (recordsForUpdate.Any())
|
||||
if (finalRecordsForUpdate.Any())
|
||||
{
|
||||
var updateData = recordsForUpdate.ToDictionary(
|
||||
var updateData = finalRecordsForUpdate.ToDictionary(
|
||||
r => r.entityId,
|
||||
r => r.transformedData);
|
||||
updateTask = salesforceClient.BatchUpdateEntitiesAsync(selectedRestEntity.Name, updateData);
|
||||
@@ -2608,10 +2634,13 @@ public partial class DataCoupler : ComponentBase
|
||||
int errorCount = 0;
|
||||
int updatedCount = 0;
|
||||
|
||||
// Lista per raccogliere le task di creazione associazioni
|
||||
var createAssociationTasks = new List<Task>();
|
||||
|
||||
for (int i = 0; i < createResults.Count; i++)
|
||||
{
|
||||
var result = createResults[i];
|
||||
var originalData = recordsForCreate[i];
|
||||
var originalData = finalRecordsForCreate[i];
|
||||
|
||||
var transferResult = new TransferResult
|
||||
{
|
||||
@@ -2626,10 +2655,12 @@ public partial class DataCoupler : ComponentBase
|
||||
transferResult.Message = "Record inserito con successo (Composite)";
|
||||
transferResult.EntityId = result.EntityId;
|
||||
|
||||
// Crea associazione se necessario
|
||||
// Aggiungi task di creazione associazione alla lista (esecuzione parallela)
|
||||
if (useRecordAssociations && !string.IsNullOrEmpty(transferResult.EntityId))
|
||||
{
|
||||
await CreateAssociationAsync(originalData.originalRecord, transferResult.EntityId, originalData.recordNumber);
|
||||
// IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela
|
||||
var associationTask = CreateAssociationAsync(originalData.originalRecord, transferResult.EntityId, originalData.recordNumber);
|
||||
createAssociationTasks.Add(associationTask);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2643,10 +2674,13 @@ public partial class DataCoupler : ComponentBase
|
||||
}
|
||||
|
||||
// 5. Processa i risultati degli aggiornamenti
|
||||
// Lista per raccogliere le task di aggiornamento associazioni
|
||||
var updateAssociationTasks = new List<Task>();
|
||||
|
||||
for (int i = 0; i < updateResults.Count; i++)
|
||||
{
|
||||
var result = updateResults[i];
|
||||
var originalData = recordsForUpdate[i];
|
||||
var originalData = finalRecordsForUpdate[i];
|
||||
|
||||
var transferResult = new TransferResult
|
||||
{
|
||||
@@ -2661,10 +2695,12 @@ public partial class DataCoupler : ComponentBase
|
||||
transferResult.Message = $"Record aggiornato con successo (Composite) - ID: {result.EntityId}";
|
||||
transferResult.EntityId = result.EntityId;
|
||||
|
||||
// Aggiorna l'associazione
|
||||
// Aggiungi task di aggiornamento associazione alla lista (esecuzione parallela)
|
||||
if (useRecordAssociations && !string.IsNullOrEmpty(result.EntityId))
|
||||
{
|
||||
await UpdateAssociationVerificationAsync(result.EntityId);
|
||||
// IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela
|
||||
var verificationTask = UpdateAssociationVerificationAsync(result.EntityId);
|
||||
updateAssociationTasks.Add(verificationTask);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2673,17 +2709,38 @@ public partial class DataCoupler : ComponentBase
|
||||
transferResult.Status = "error";
|
||||
transferResult.Message = $"Errore aggiornamento (Composite): {result.ErrorMessage}";
|
||||
|
||||
// Elimina associazione non valida se l'aggiornamento fallisce
|
||||
// Aggiungi task di gestione fallimento alla lista (esecuzione parallela)
|
||||
if (useRecordAssociations)
|
||||
{
|
||||
await HandleFailedUpdateAsync(originalData.originalRecord, originalData.recordNumber);
|
||||
// IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela
|
||||
var failureTask = HandleFailedUpdateAsync(originalData.originalRecord, originalData.recordNumber);
|
||||
updateAssociationTasks.Add(failureTask);
|
||||
}
|
||||
}
|
||||
|
||||
transferResults.Add(transferResult);
|
||||
}
|
||||
|
||||
// 6. Mostra risultati
|
||||
// 6. Esegui tutte le operazioni di associazione in parallelo
|
||||
var allAssociationTasks = createAssociationTasks.Concat(updateAssociationTasks).ToList();
|
||||
if (allAssociationTasks.Any())
|
||||
{
|
||||
Logger.LogInformation("COMPOSITE: Avvio di {TaskCount} operazioni di associazione in parallelo ({CreateCount} creazioni, {UpdateCount} aggiornamenti) usando DbContext separati",
|
||||
allAssociationTasks.Count, createAssociationTasks.Count, updateAssociationTasks.Count);
|
||||
|
||||
var startTime = DateTime.UtcNow;
|
||||
await Task.WhenAll(allAssociationTasks);
|
||||
var endTime = DateTime.UtcNow;
|
||||
|
||||
Logger.LogInformation("COMPOSITE: Operazioni di associazione completate in {ElapsedMs}ms con esecuzione parallela reale",
|
||||
(endTime - startTime).TotalMilliseconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInformation("COMPOSITE: Nessuna operazione di associazione da eseguire");
|
||||
}
|
||||
|
||||
// 7. Mostra risultati
|
||||
ShowTransferResults(successCount, updatedCount, 0, errorCount);
|
||||
|
||||
Logger.LogInformation("Trasferimento COMPOSITE completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Errori: {ErrorCount}",
|
||||
@@ -2705,6 +2762,13 @@ public partial class DataCoupler : ComponentBase
|
||||
{
|
||||
try
|
||||
{
|
||||
// Cattura i valori condivisi all'inizio per evitare race conditions
|
||||
var currentSourceKeyField = sourceKeyField;
|
||||
var currentEntityName = selectedRestEntity?.Name ?? "";
|
||||
var currentCredentialName = selectedRestCredential ?? "";
|
||||
var currentMappingCount = fieldMappings.Count;
|
||||
var currentSourceType = selectedSourceType;
|
||||
|
||||
var sourceKey = GenerateSourceKey(originalRecord);
|
||||
if (string.IsNullOrEmpty(sourceKey)) return;
|
||||
|
||||
@@ -2712,25 +2776,25 @@ public partial class DataCoupler : ComponentBase
|
||||
var association = new KeyAssociation
|
||||
{
|
||||
KeyValue = sourceKey,
|
||||
SourceKeyField = sourceKeyField,
|
||||
SourceKeyField = currentSourceKeyField,
|
||||
DestinationKeyField = destinationKeyField,
|
||||
DestinationEntity = selectedRestEntity?.Name ?? "",
|
||||
DestinationEntity = currentEntityName,
|
||||
DestinationId = entityId,
|
||||
RestCredentialName = selectedRestCredential,
|
||||
RestCredentialName = currentCredentialName,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
LastVerifiedAt = DateTime.UtcNow,
|
||||
AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
TransferDate = DateTime.UtcNow,
|
||||
RecordNumber = recordNumber,
|
||||
MappingCount = fieldMappings.Count,
|
||||
SourceType = selectedSourceType,
|
||||
MappingCount = currentMappingCount,
|
||||
SourceType = currentSourceType,
|
||||
CompositeTransfer = true
|
||||
})
|
||||
};
|
||||
|
||||
var associationId = await CredentialService.SaveKeyAssociationAsync(association);
|
||||
Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber}", associationId, recordNumber);
|
||||
var associationId = await CredentialService.SaveKeyAssociationParallelAsync(association);
|
||||
Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber} (PARALLEL)", associationId, recordNumber);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -2761,12 +2825,12 @@ public partial class DataCoupler : ComponentBase
|
||||
var sourceKey = GenerateSourceKey(originalRecord);
|
||||
if (string.IsNullOrEmpty(sourceKey)) return;
|
||||
|
||||
var existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(
|
||||
var existingAssociation = await CredentialService.FindKeyAssociationByValueParallelAsync(
|
||||
sourceKey, selectedRestEntity?.Name ?? "", selectedRestCredential ?? "");
|
||||
|
||||
if (existingAssociation != null)
|
||||
{
|
||||
await CredentialService.DeleteKeyAssociationAsync(existingAssociation.Id);
|
||||
await CredentialService.DeleteKeyAssociationParallelAsync(existingAssociation.Id);
|
||||
Logger.LogInformation("COMPOSITE: Associazione non valida eliminata per record {RecordNumber}", recordNumber);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@page "/"
|
||||
@* @page "/"
|
||||
|
||||
<PageTitle>Index</PageTitle>
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
|
||||
Welcome to your new app.
|
||||
|
||||
<SurveyPrompt Title="How is Blazor working for you?" />
|
||||
<SurveyPrompt Title="How is Blazor working for you?" /> *@
|
||||
|
||||
Reference in New Issue
Block a user