【发布时间】:2020-07-28 07:55:31
【问题描述】:
我有一个使用 Entity Framework Core 的 .Net Core 3.1 Web API。它使用 HTTP Polling (https://docs.microsoft.com/en-us/azure/architecture/patterns/async-request-reply),因此可以调用一个端点来开始一个进程,另一个端点返回进程的状态,另一个端点返回结果。简而言之,第一个调用将很快返回,但会启动一个在返回后完成的过程。当这个过程完成时,我需要将一些东西保存到数据库中,但我的 dbContext 已经被处理掉了。
这是完整的细节:
Controller 操作将要求注入的服务启动进程(A 计算),然后它会返回端点以调用以获取有关该进程的更多信息:
[HttpPost]
public ActionResult<CalculationResource> createNewCalculationRequest([FromBody, BindRequired] CalculationRequestResource calculationRequest)
{
var calc = _service.newCalculation(calculationRequest.Formats, calculationRequest.ParticipantId, calculationRequest.CaseId, calculationRequest.Input);
var calcResponse = _mapper.Map<CalculationResource>(calc);
return CreatedAtAction(nameof(getCalculation), new { calculationId = calc.Id }, calcResponse);
}
该服务将保存该计算,然后要求驱动程序启动它。驱动程序将同步返回进程已启动(或未启动)。此时响应已发送,但当驱动程序被要求启动计算时,它被赋予了一个在进程完成时调用的函数,该函数更新状态并设置结果。此函数将在响应已发送后执行。
这是服务代码:
public CalculationService(CalculationsContext context, ICalculationDriver driver, ILogger<CalculationService> logger)
{
this._context = context;
this._driver = driver;
this._logger = logger;
}
public Calculation newCalculation(ISet<Format> requestedFormats, string participantId, string caseId, string input)
{
var now = DateTime.Now;
var newCalcId = CreateNewCalculationId(caseId, participantId, now);
var newCalc = new Calculation(newCalcId, caseId, participantId, requestedFormats, now);
var newInput = new CalculationInput(newCalcId, input);
_context.Calculations.Add(newCalc);
_context.CalculationInputs.Add(newInput);
foreach(Format format in requestedFormats)
{
var result = new CalculationResult(newCalcId, format, Status.Needed, null);
_context.CalculationResults.Add(result);
}
var calcStartedInfos = _driver.LaunchCalculation(newCalc.Id, input, requestedFormats, (statusUpdate) => {
//Everything in here is executed later. It is a function passed to the driver which the driver calls to alert this service of status updates
try
{
//Results and Status exist on the same entity, so we might as well grab it once, up here
var resultEntity = _context.CalculationResults.Find(newCalcId, statusUpdate.Format);
//If it completed successfully, we need to save the results
if (statusUpdate.Status == Status.CompletedOk)
{
//Get the results from the driver
using Stream resultStream = _driver.RetrieveResults(statusUpdate.CalculationId, statusUpdate.Format);
using CryptoStream resultStreamEncoded = new CryptoStream(resultStream, new ToBase64Transform(), CryptoStreamMode.Read);
using StreamReader sr = new StreamReader(resultStreamEncoded);
String result = sr.ReadToEnd();
//Update the repo with them
resultEntity.Result = result;
}
//Save the status, whatever it is
_logger.LogDebug("Updating status for calculation {calcId} format {format} to {status}", statusUpdate.CalculationId, statusUpdate.Format, statusUpdate.Status);
resultEntity.Status = statusUpdate.Status;
//Finally, persist changes to repo
_context.SaveChanges();
} catch (Exception e)
{
//If we don't eat an exception within this block and we allow it to bubble up, the entire process will crash
_logger.LogError(e, "An exception occurred while handling a status update from the driver");
try
{
var resultEntity = _context.CalculationResults.Find(newCalcId, statusUpdate.Format);
resultEntity.Status = Status.CompletedErrorsDetected;
_context.SaveChanges();
} catch (Exception e2)
{
_logger.LogError(e2, "Failed to update calculation status after encountering an exception while handling a status update");
}
}
});
foreach(CalculationStartedInfo startInfo in calcStartedInfos)
{
//Each startInfo represents one format, so let's grab the entity for that format
var resultEntity = _context.CalculationResults.Find(newCalcId, startInfo.Format);
if (startInfo.Started)
{
_logger.LogTrace("Detected Calculation process start successful for calcId: {calculationId} format: {format}", startInfo.CalculationId, startInfo.Format);
//Set the status to indicate that the calculation has actually started
resultEntity.Status = Status.ProceedingNormally;
} else
{
_logger.LogError("Detected Calculation process start failed for calcId: {calculationId} format: {format}", startInfo.CalculationId, startInfo.Format);
//Set the status to indicate that the calculation couldn't even start
resultEntity.Status = Status.CompletedErrorsDetected;
}
}
_context.SaveChanges();
return newCalc;
}
驱动程序处理与实际执行计算的 EXE 的通信。它为计算中的每个格式创建该 EXE 的新进程(稍微简化,但应该足够好)并附加一个退出处理程序,该处理程序调用服务传入的回调函数。这是代码(为 SO简化一些不相关的细节):
public IEnumerable<CalculationStartedInfo> LaunchCalculation(string calculationId, string data, IEnumerable<Format> formats, Action<CalculationStatusUpdate> StatusChanged)
{
if (StatusChanged == null)
{
throw new ArgumentNullException(nameof(StatusChanged), "Status Update function must not be null");
}
/* We have to report the initial startup status synchronously because if we report it with a call to StatusChanged
* it appears to create a race condition against the StatusChanged that is called on Exit */
List<CalculationStartedInfo> startedReport = new List<CalculationStartedInfo>();
foreach(Format format in formats)
{
//If we use "using" here, the process will be disposed of before the Exited handler is called
Process p = new Process();
try
{
ProcessStartInfo startInfo = new ProcessStartInfo()
{
//Basically we're calling out to an EXE to do the calculations
};
p.StartInfo = startInfo;
p.EnableRaisingEvents = true;
p.Exited += (object sender, EventArgs e) =>
{
Process endingP = (Process)sender;
string calcId = endingP.StartInfo.Environment[FOO_BAR];
int actualPid = endingP.Id;
int exitCode = endingP.ExitCode;
endingP.Dispose();
_logger.LogDebug("Process {processId} for calculation id {calculationId} has terminated with code {exitCode}.", actualPid, calcId, exitCode);
if (exitCode == 0)
{
StatusChanged(calculationId, format, Status.CompletedOk);
}
else
{
StatusChanged(calculationId, format, Status.CompletedErrorsDetected);
}
};
bool isStarted = p.Start();
if (isStarted)
{
_logger.LogDebug("Started {processName} with process id {processId} for calculation id {calculationId}", p.ProcessName, p.Id, calculationId);
//Adds a CalculationStartedInfo that reports this Format was successfully started
}
else
{
_logger.LogError("Failed to start process");
//Adds a CalculationStartedInfo that reports this Format was NOT successfully started
}
}
catch (Exception e)
{
_logger.LogError(e, "Exception while starting calculation process: " + e.Message);
p.Dispose();
throw e;
}
}
return startedReport;
}
重申问题:在回调内部使用dbContext时,响应已经发送后,由于已经被释放,所以抛出异常。
【问题讨论】:
标签: c# .net-core entity-framework-core