Parallel.For exporting problems

Hi all,

We are using latest nuget available MIPS packages, 23.2.1.

we are experiencing troubles using NetFramework 4.7.2 TPE Parallel.For to perform parallel export;

the problem is not costant, but it actually appears from time to time, indicating in the event viewer

and

The Parallel.For code is

ConcurrentBag<string> resultCollection = new ConcurrentBag<string>();
 
int CYCLE = 3;
 
int min = 0;
int max = min + CYCLE;
if (max > sequences.Count)
{
	max = sequences.Count;
}
 
int loop = 0;
while (min < sequences.Count)
{
	loop++;
	_log.Debug($"{(e.IsRecovery ? "Recovering Export" : "Exporting")}: Parallel loop # {loop}...");
 
	_log.Debug($"Parallel loop: Max CYCLE # {CYCLE} - Min loop elem: {min } - Max loop elem: {max}...");
 
	Parallel.For(min, max,
		index =>
			{
				List<string> list = DoExportWork(new ExportWorkArg()
				{
					Connection = e.Connection,
					FindCamera = findcamera,
 
					Folder = folder,
 
					IsRecovery = e.IsRecovery,
					CameraTimeFrame = e.CameraTimeFrame,
 
					SequenceNro = index + 1,
					SequencesCount = sequences.Count,
					LoopNo = loop,
 
					Slot = sequences[index],
					LogStart = e.LogStart,
					LogEnd = e.LogEnd,
				});
 
				foreach (string s in list)
				{
					resultCollection.Add(s);
				}
			});
 
	allFiles.AddRange(resultCollection);
 
	min = max + 1;
	max = min + CYCLE;
	if (max > sequences.Count)
	{
		max = sequences.Count;
	}
}

And the actual exporting Parallel.For calls is perfomed like

private List<string> DoExportWork(ExportWorkArg e)
{
	List<string> result = new List<string>();
	string sequenceFileName = Utilities.BuildFileName(e.FindCamera, e.Slot.From, e.Slot.To, Utilities.file_datetimeformat);
 
	DateTime recTime = DateTime.Now;
	//genera inizio e fine da inserire nel nome file e per fare l'acquisizione
	DateTime slotStart = e.Slot.From;
	DateTime slotEnd = e.Slot.To.AddMilliseconds(-1);
 
	_log.Debug($"{(e.IsRecovery ? "Recovering Export" : "Exporting")}{(e.LoopNo != 0 ? $" Loop: {e.LoopNo} - " : "")} sequence # {e.SequenceNro} / {e.SequencesCount} : {e.LogStart.ToString(Utilities.log_datetimeformat)}-{e.LogEnd.ToString(Utilities.log_datetimeformat)} >> seq: {e.Slot.From.ToString(Utilities.log_datetimeformat)}-{e.Slot.To.ToString(Utilities.log_datetimeformat)}");
 
	TimeSpan tsExport = e.Slot.To - e.Slot.From;
	if (e.CameraTimeFrame < tsExport.TotalMinutes)
	{
		_log.Error($"{(e.IsRecovery ? "Recovering Export" : "Exporting")} camera [{e.FindCamera.FQID.ObjectId}]{(e.LoopNo != 0 ? $" Loop: {e.LoopNo} - " : "")} sequence # {e.SequenceNro} / {e.SequencesCount} : {e.LogStart.ToString(Utilities.log_datetimeformat)}-{e.LogEnd.ToString(Utilities.log_datetimeformat)} >> seq: {e.Slot.From.ToString(Utilities.log_datetimeformat)}-{e.Slot.To.ToString(Utilities.log_datetimeformat)} - ***WARNING*** tsExport: {tsExport.TotalMinutes} - cameraTimeFrame: {e.CameraTimeFrame}!");
	}
 
	{
		try
		{
			e.Connection.Export(e.FindCamera,
				slotStart,
				slotEnd,
				e.Folder,
				$"{sequenceFileName}.mkv");
		}
		catch (Exception ex)
		{
			throw ex;
		}
	}
 
	TimeSpan tsRecTime = DateTime.Now - recTime;
	_log.Debug($"{(e.IsRecovery ? "Recovering Export" : "Exporting")}: File exported successfully! ({tsRecTime.TotalMilliseconds.ToString("N0")} msec.){(e.LoopNo != 0 ? $" - Loop: {e.LoopNo} " : "")}- sequence # {e.SequenceNro} / {e.SequencesCount} : {e.LogStart.ToString(Utilities.log_datetimeformat)}-{e.LogEnd.ToString(Utilities.log_datetimeformat)} >> seq: {e.Slot.From.ToString(Utilities.log_datetimeformat)}-{e.Slot.To.ToString(Utilities.log_datetimeformat)}");
 
	string sequenceJsonFileName = $"{sequenceFileName}.json";
	var slotSequences = new List<MilestoneExporterSequence>();
	slotSequences.Add(e.Slot);
 
	var jsonData = new
	{
		Sha256CRC = MilestoneExporter.Controller.Utilities.SHA256CheckSum(System.IO.Path.Combine(e.Folder, $"{sequenceFileName}.mkv")),
		Sequences = slotSequences,
	};
	string jsonText = JsonConvert.SerializeObject(jsonData, formatting: Formatting.Indented);
 
	string jsonFile = $@"{e.Folder}\{sequenceFileName}.json";
	_log.Debug($"{(e.IsRecovery ? "Recovering Export" : "Exporting")}: Creating file {jsonFile} {(e.LoopNo != 0 ? $" - Loop: {e.LoopNo} " : "")}- sequence # {e.SequenceNro} / {e.SequencesCount}...");
	File.WriteAllText(jsonFile, jsonText);
	_log.Debug($"{(e.IsRecovery ? "Recovering Export" : "Exporting")}: File created successfully {(e.LoopNo != 0 ? $" - Loop: {e.LoopNo} " : "")}- sequence # {e.SequenceNro} / {e.SequencesCount}!");
 
	result.Add($"{System.IO.Path.Combine(e.Folder, sequenceFileName + ".mkv")}");
	result.Add($"{System.IO.Path.Combine(e.Folder, sequenceFileName + ".json")}");
 
	return result;
}

which, finally, calls the export routine,

public void Export(Item camera, DateTime from, DateTime to, string folder, string filename)
{
	_exporter = new VideoOS.Platform.Data.MKVExporter() { Filename = filename };
	_exporter.Init();
	_exporter.Path = folder;
	var temp = new VideoOS.Platform.Item();
	_exporter.CameraList = new List<VideoOS.Platform.Item>() { camera };
	//_exporter.AudioList = camera.GetRelated().Where(x => x.FQID.Kind == Kind.Microphone || x.FQID.Kind == Kind.Speaker).ToList();
	_exporter.AudioList = camera.GetRelated().Where(x => x.FQID.Kind == Kind.Microphone).ToList();
 
	try
	{
		_exporter.StartExport(from, to);
		_exporter.EndExport();
	}
	catch (Exception ex)
	{
		throw ex;
	}
}

As you can see, the whole code is performed in Try/Catch blocks, and the initial Parallel.For is inserted in an additional Try/Catch block running in a loop of selected Camera items, but the code “just breaks” with the indications in the event viewer and the ErrorHandlers are not called…

Btw, we added even initial “uncatched exceptions” handlers,

public partial class MilestoneExporterService : ServiceBase
{
	private Logger log;
	private string servicepath = AppDomain.CurrentDomain.BaseDirectory;
	private MilestoneExportServiceLibrary svc;
	public MilestoneExporterService()
	{
		InitializeComponent();
		this.CanHandlePowerEvent = true;
 
		System.Windows.Forms.Application.ThreadException += new ThreadExceptionEventHandler(ApplicationThreadException);
		AppDomain.CurrentDomain.UnhandledException += DomainUnhandledException;
	}
 
	private void DomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
	{
		if (log != null)
		{
			log.Error($"FATAL DomainUnhandledException - {((Exception)e.ExceptionObject).Message}");
			log.Error($"StackTrace: {((Exception)e.ExceptionObject).StackTrace}");
		}
	}
 
	private void ApplicationThreadException(object sender, ThreadExceptionEventArgs t)
	{
		if (log != null)
		{
			log.Error($"FATAL ApplicationThreadException - {t.Exception.Message}");
		}
	}
 
	protected override void OnStart(string[] args)
	{
	....

But those 2 handlers are not called at all, and our service just “shuts down”….

Thanking you in advance for your kind support, and please excuse my poor English…

--

Andrea

hi all,

in order to minimize the “code review”, I do here “(over)simplify” it, removing all un-necessary stuff (sorry for that in the previous :smiley: ) and some “pseudo code” is inserted to clarify some points/actions…

OurService.cs {
 
	private async void Timer_export_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
	{
		// main timer loop....
		
		if (timerMeetExportingRequirements...)
		{
			timer.stop();
			try			
			{				
				var connection = openMilestoneServerConnection...();
				
				foreach (var item in CamerasList)
				{
					try
					{
						await ExportPerform(all required params...);
					}
					catch (Exception ex)
					{
						clean up code....
					}
				}
			}
            catch (Exception ex)
            {
				just logging...
            }
            finally
            {
				close milestoneConn...
				
				timer.Start();
            }
		}
	}
	
	private async Task ExportPerform(all required params....)
	{
		try
        {		
			var sequences = GettingRecordingSequencesInTheSpecifiedTimeSlot(params....);
		
			List<ExportWorkResult> allWorksResult = new List<ExportWorkResult>();
			ConcurrentBag<ExportWorkResult> resultCollection = new ConcurrentBag<ExportWorkResult>();
 
			var options = new ParallelOptions()
			{
				MaxDegreeOfParallelism = 10
			};
 
			Parallel.For(0, sequences.Count, options,
				index =>
				{
					ExportWorkResult result = DoExportWork(new ExportWorkArg()
					{
						params.....
					});
					
					resultCollection.Add(result);
				});
 
			allWorksResult.AddRange(resultCollection);
			
			DoSomeStuffWithAllWorksResult....;			
		}
	}
 
	private ExportWorkResult DoExportWork(ExportWorkArg e)
	{
		ExportWorkResult workResult = new ExportWorkResult();
		
		string sequenceFileName = Utilities.BuildFileName(e.FindCamera, e.Slot.From, e.Slot.To, Utilities.file_datetimeformat);
 
		DateTime recTime = DateTime.Now;
		DateTime slotStart = e.Slot.From;
		DateTime slotEnd = e.Slot.To.AddMilliseconds(-1);
 
		try
		{
			e.Connection.Export(_log,
				e.FindCamera,
				slotStart,
				slotEnd,
				e.Folder,
				$"{sequenceFileName}.mkv");
		}
		catch (Exception ex)
		{
			throw ex;
		}
 
 
		System.Threading.Thread.Sleep(250);
 
		populate workResult...
 
		return workResult;
	}	
}
 
other module e.Connection
{
	public void Export(Serilog.ILogger logger, Item camera, DateTime from, DateTime to, string folder, string filename)
	{
		_exporter = new VideoOS.Platform.Data.MKVExporter() { Filename = filename };
		_exporter.Init();
		_exporter.Path = folder;
		var temp = new VideoOS.Platform.Item();
		_exporter.CameraList = new List<VideoOS.Platform.Item>() { camera };
		_exporter.AudioList = camera.GetRelated().Where(x => x.FQID.Kind == Kind.Microphone).ToList();
 
		try
		{
			_exporter.StartExport(from, to);
			_exporter.EndExport();
 
			System.Threading.Thread.Sleep(100);
 
		}
		catch (Exception ex)
		{
			throw ex;
		}
	}
}

again, the service boot registered 2 unhandledExceptions event, both for

System.Windows.Forms.Application.ThreadException += new ThreadExceptionEventHandler(ApplicationThreadException);

AppDomain.CurrentDomain.UnhandledException += DomainUnhandledException;

BTW, another blocking untrapped error results form time to time in the event viewer, related to ucrtbase.dll

thanking you in advance…

--

Andrea

The export toolkit utilized by the Exporter in MIP SDK is not thread safe. This means you cannot run the Exporter multiple times in parallel.

I wonder if you could make a design where you have separate processes for each export.

Good morning Bo,

and thank you for your kind input.

>I wonder if you could make a design where you have separate processes for each export.

can you please expand on that? just a minimal blueprint to follow?

TIA

--

Andrea

Two Smart Clients can export at the same time. If you create and run two instances of your executable they could work exporting at the same time.

One of Milestone’s expert developers is doing a deeper investigation as there are a drive here to improve the exporter if possible.

The developer was unfortunately unable to reproduce the issue, even increasing the parallel and letting it run all night he did not see the issue.

As you have been implementing new code on my recommendation I realize what we now ask might not be practical or convenient. If you can supply the information It would be a help..

Developer ask:

How many they are exporting and seeing the problem? Does it happen only sporadically?

Additionally, can they run procdump to generate a crash dump (procdump -e -ma <their-process-name.exe>) and send it to us?

--

If you say you do not have the old system in a way where it will be easy to answer and supply the dump we will understand, if you are able and willing to help us we will appreciate it!

(Also posted on MSC1743870)