feat(v4): consolidate audited data recovery economic replay and optional archival

This commit is contained in:
ENELIX Agent
2026-10-03 10:40:18 +00:00
parent 6c91d6bcc6
commit 8eb9688942
26 changed files with 1747 additions and 38 deletions
+41 -7
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Belevo\EnelixEMS;
require_once __DIR__ . '/NetzfahrplanV4Messaufnahme.php';
require_once __DIR__ . '/NetzfahrplanV4Datenarchiv.php';
/** Application measurement acquisition + acknowledged durable delivery. No device writes. */
trait ManagerNetzfahrplanV4DatenTrait
@@ -11,9 +12,13 @@ trait ManagerNetzfahrplanV4DatenTrait
private function registriereNetzfahrplanV4Daten(): void
{
$this->RegisterPropertyBoolean('NetzfahrplanV4MessdatenAktiv', false);
$this->RegisterPropertyBoolean('NetzfahrplanV4ArchivAktiv', false);
$this->RegisterPropertyString('NetzfahrplanV4Messkonfiguration', '{}');
$this->RegisterPropertyString('NetzfahrplanV4Datensatz', '');
$this->RegisterAttributeString('NetzfahrplanV4DatenCursor', '{}');
$this->RegisterAttributeString('NetzfahrplanV4LetzteDatenquittung', '{}');
$this->RegisterAttributeString('NetzfahrplanV4ArchivTag', '');
$this->RegisterAttributeInteger('NetzfahrplanV4DatenVersion', 0);
$this->RegisterAttributeInteger('NetzfahrplanV4DatenVersuch', 0);
$this->RegisterAttributeInteger('NetzfahrplanV4DatenFehlerzahl', 0);
$this->RegisterVariableString('NetzfahrplanV4Datenstatus', 'V4 Messdaten und Modellversorgung', '', 199);
@@ -27,6 +32,12 @@ trait ManagerNetzfahrplanV4DatenTrait
if (!$this->ReadPropertyBoolean('NetzfahrplanV4MessdatenAktiv')) return;
try {
$this->v4DatenKonfiguration();
if ($this->ReadAttributeInteger('NetzfahrplanV4DatenVersion')<1) {
// One retry after the data-only upgrade, never a cursor reset or a device write.
$this->WriteAttributeInteger('NetzfahrplanV4DatenVersuch', 0);
$this->WriteAttributeInteger('NetzfahrplanV4DatenFehlerzahl', 0);
$this->WriteAttributeInteger('NetzfahrplanV4DatenVersion', 1);
}
$this->SetTimerInterval('NetzfahrplanV4DatenErfassen', 30000);
} catch (\Throwable $e) {
$this->v4DatenStatus(['status'=>'configuration_error','errorType'=>get_class($e)]);
@@ -60,6 +71,8 @@ trait ManagerNetzfahrplanV4DatenTrait
private function v4DatenStatus(array $data): void
{
$receipt=json_decode($this->ReadAttributeString('NetzfahrplanV4LetzteDatenquittung'),true,32,JSON_THROW_ON_ERROR);
$data['lastAcknowledgement']=$receipt;
$this->SetValue('NetzfahrplanV4Datenstatus',json_encode($data+['checkedAt'=>gmdate('c'),
'datasetId'=>$this->ReadPropertyString('NetzfahrplanV4Datensatz'),'controlEnabled'=>false],JSON_THROW_ON_ERROR));
}
@@ -78,11 +91,25 @@ trait ManagerNetzfahrplanV4DatenTrait
return ['value'=>GetValue($id),'updated'=>(int)$v['VariableUpdated'],'changed'=>(int)$v['VariableChanged'],
'parentID'=>$o['ParentID'],'ident'=>$o['ObjectIdent']];
};
$r=NetzfahrplanV4Messaufnahme::capture($c,$reader,static fn():int=>time());
$dir=$this->v4DatenVerzeichnis();
NetzfahrplanV4Messaufnahme::append($dir,$r);
$result=['status'=>'recorded','capturedAt'=>$r['capturedAt'],'sourceCount'=>count($r['raw']),
'rawIssues'=>count($r['issues']),'measurementBoundaryVerified'=>false];
$archiveResult=null;
if ($this->ReadPropertyBoolean('NetzfahrplanV4ArchivAktiv') && $this->ReadAttributeString('NetzfahrplanV4ArchivTag')!==gmdate('Y-m-d')) {
try {
$archiveCursor=json_decode($this->ReadAttributeString('NetzfahrplanV4DatenCursor'),true,32,JSON_THROW_ON_ERROR);
$archiveResult=NetzfahrplanV4Datenarchiv::run($dir,$archiveCursor,time());
if ($archiveResult['archived']<2 && ($archiveResult['status']??null)!=='writer_busy')$this->WriteAttributeString('NetzfahrplanV4ArchivTag',gmdate('Y-m-d'));
} catch (\Throwable $e) { $archiveResult=['status'=>'archive_error','errorType'=>get_class($e),'originalsRetained'=>true]; }
}
try {
$r=NetzfahrplanV4Messaufnahme::capture($c,$reader,static fn():int=>time());
NetzfahrplanV4Messaufnahme::append($dir,$r);
$result=['status'=>'recorded','capturedAt'=>$r['capturedAt'],'sourceCount'=>count($r['raw']),
'rawIssues'=>count($r['issues']),'measurementBoundaryVerified'=>false];
} catch (\Throwable $e) {
// Storage/capture failure must not prevent retrying the already durable outbox.
$result=['status'=>'capture_error','errorType'=>get_class($e),'dataRetained'=>true];
}
if ($archiveResult!==null)$result['archive']=$archiveResult;
$last=$this->ReadAttributeInteger('NetzfahrplanV4DatenVersuch');
$failures=min(5,$this->ReadAttributeInteger('NetzfahrplanV4DatenFehlerzahl'));
$retry=min(900,60*(2**$failures));
@@ -90,10 +117,16 @@ trait ManagerNetzfahrplanV4DatenTrait
$this->WriteAttributeInteger('NetzfahrplanV4DatenVersuch',time());
try {
$result['delivery']=$this->v4DatenUebertragen($dir);
if (($result['delivery']['status']??null)==='acknowledged') {
$this->WriteAttributeString('NetzfahrplanV4LetzteDatenquittung',json_encode($result['delivery']+['receivedAt'=>gmdate('c')],JSON_THROW_ON_ERROR));
}
$this->WriteAttributeInteger('NetzfahrplanV4DatenFehlerzahl',0);
} catch (\Throwable $e) {
$this->WriteAttributeInteger('NetzfahrplanV4DatenFehlerzahl',$failures+1);
$result['delivery']=['status'=>'retry_pending','errorType'=>get_class($e),'dataRetained'=>true];
$httpCode=(int)$e->getCode();
$result['delivery']=['status'=>'retry_pending','errorType'=>get_class($e),
'httpStatus'=>$httpCode>=100&&$httpCode<=599?$httpCode:null,'dataRetained'=>true,
'nextAttemptAt'=>gmdate('c',time()+min(900,60*(2**min(5,$failures+1))))];
}
} else $result['delivery']=['status'=>'scheduled','nextAttemptAt'=>gmdate('c',$last+$retry)];
$this->v4DatenStatus($result);
@@ -147,13 +180,14 @@ trait ManagerNetzfahrplanV4DatenTrait
CURLOPT_HTTPHEADER=>['Content-Type: application/json','Accept: application/json','Authorization: Bearer '.$token],
CURLOPT_POSTFIELDS=>$payload]);
$answer=curl_exec($h);$code=(int)curl_getinfo($h,CURLINFO_HTTP_CODE);
if ($answer===false||$code!==200||strlen($answer)>65536) throw new \RuntimeException('Measurement delivery not acknowledged');
if ($answer===false||$code!==200||strlen($answer)>65536) throw new \RuntimeException('Measurement delivery not acknowledged',$code);
$ack=json_decode($answer,true,32,JSON_THROW_ON_ERROR);
if (!in_array($ack['status']??null,['stored','duplicate'],true)
||($ack['datasetId']??null)!==$this->ReadPropertyString('NetzfahrplanV4Datensatz')
||($ack['acceptedThrough']??null)!==$lastTime) throw new \RuntimeException('Measurement acknowledgement does not match batch');
$this->WriteAttributeString('NetzfahrplanV4DatenCursor',json_encode($candidate,JSON_THROW_ON_ERROR));
return ['status'=>'acknowledged','records'=>count($records),'acceptedThrough'=>$lastTime];
return ['status'=>'acknowledged','records'=>count($records),'acceptedThrough'=>$lastTime,
'datasetId'=>$this->ReadPropertyString('NetzfahrplanV4Datensatz')];
} finally { curl_close($h); }
}
}