Add HistorianGrpcStoreForwardStatusProbe and the `grpc-sf-status-probe` CLI command. The idle-baseline run against the live 2023 R2 server resolves the plan's §9.3 handle question: the direct StorageService SF pull RPCs (GetSFParameter / GetRemainingSnapshotsSize) require the OpenStorageConnection console handle and are D2-gated (err 132, identical under read-only and write-enabled sessions), while StatusService.GetHistorianConsoleStatus IS reachable on the session string handle (=3 at idle). Records the gRPC re-scope and the idle-baseline findings in docs/plans/store-forward-cache-reverse-engineering.md §9. The probe writes nothing and releases any console session immediately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
35 KiB
Store/Forward Cache Reverse-Engineering Plan
Last updated: 2026-06-21
2026-06-21 R4.3 re-scope — read this first. The original plan below (2026-05-04) was written against the 2020 Net.TCP/WCF transport, before the 2023 R2 gRPC transport existed. Its single biggest open risk — "is SF state readable via a one-shot pull, or only via a duplex push contract we'd have to add?" (Q1/Q2 + §3 Step 3 + Risk 4) — is now answered: pull, no duplex. The recovered gRPC
StorageServicecontract exposes SF state as plain request/response RPCs. The current R4.3 scope and recommended path are in §9 ("2026-06-21 gRPC re-scope"); the 2020-WCF body below is retained as background, not the recommended route.
Original last-updated: 2026-05-04
This document plans the reverse-engineering effort needed to replace the
synthesized GetStoreForwardStatusAsync in
src/AVEVA.Historian.Client/Wcf/HistorianWcfStatusClient.cs (lines 101-117)
with a real, evidence-backed implementation. It is a plan, not the work
itself. No code changes; no captures collected.
Read this together with:
docs/reverse-engineering/handoff.md— read/event protocol decoding statesrc/AVEVA.Historian.Client/Wcf/Contracts/IStorageServiceContract.cs— the WCF contract that already declares the SF parameter opssrc/AVEVA.Historian.Client/Models/HistorianStoreForwardStatus.cs— the output model the implementation must populate
1. Goal
"SF support works" means, end-to-end:
- Primary deliverable.
client.GetStoreForwardStatusAsync()against a live local Historian returns aHistorianStoreForwardStatuswhosePending,Storing,DataStored,ErrorOccurred,Error,ServerName, andConnectionKindfields reflect actual server-reported state, not the synthesized defaults atHistorianWcfStatusClient.cs:107-117. - Secondary deliverable. The SDK can also answer the higher-level
"is SF currently buffering?" question accurately when the runtime DB is
down, not just when it is up. That is the case the real native client
handles correctly and where the synthesized default (
Storing = false,ErrorOccurred = false) is silently wrong today. - Non-goals. Writing into SF, replaying SF buffers, configuring SF
parameters, redundant-partner SF aggregation
(
HistorianStoreForwardStatus.AddPartnerStoreForwardStatus, token0x060060B8). Read-only matches the project mission inCLAUDE.md.
The success bar is parity with the native wrapper's
ArchestrA.HistorianAccess.GetStoreForwardStatus
(MD token 0x06006186 in current/aahClientManaged.dll),
not a superset.
2. Architecture Investigation (open questions, in priority order)
Answer these before writing any production code. Each has a discovery action in §3.
Q1. Is SF status read from a local in-process struct, a separate WCF endpoint, or a Named Pipe IPC?
Current evidence: all three are plausible, but the wrapper actually uses "in-process struct kept current by server-pushed WCF events". Specifically:
ArchestrA.HistorianAccess.GetStoreForwardStatus(token0x06006187, the private 2-arg overload) does not call WCF. It callsmdas_GetStorageStatus(acalliagainst theINSQL_MDAS_ERROR (IntPtr handle, uint, HISTORIAN_STORAGE_STATUS*)C signature incurrent/aahClient.dllexports) and then maps the result throughHistorianAccessUtil.ConvertUnmanagedSFStorageStatusToManagedStorageStatus(token0x060060E4).- Mutators like
CConfigStatusClient.SetMdasStoreForwardEvent(token0x060029DC) andaahClientCommon.CStatus.SetStoreForwardEvent(token0x06002A04) are wired to the WCF callbackIStatusServiceContract2.SetStoreForwardEvent(StatusServiceContract.IStatusServiceContract2.SetStoreForwardEvent, token0x06005F57). The server pushes SF state changes; the client caches them. - Confirm: read the IL of token
0x06006187and verify the only system call ismdas_GetStorageStatus. The first 200 instructions confirm this:GetClient(ConnectionIndex)→calliagainst theINSQL_MDAS_ERROR(IntPtr,uint,HISTORIAN_STORAGE_STATUS*)signature →ConvertUnmanagedSFStorageStatusToManagedStorageStatus.
Implication: the SDK cannot ship a synchronous probe that calls one WCF operation and gets the answer. It must subscribe to the same status-event stream the native wrapper subscribes to, or call a status query that returns the cached snapshot from the server.
Q2. Is there a single-shot WCF query that returns the same snapshot?
Likely yes. Hypothesis: IStatusServiceContract2.GetHistorianInfo
(GETHI, see IStatusServiceContract2.cs:24-30) returns a multi-key status
blob whose schema includes SF state. Alternative: a status-only key passed to
GetSystemParameter (already plumbed via HistorianWcfStatusClient.GetSystemParameterAsync).
Both are testable without writing protocol code by sending probe payloads
and observing the response shape.
Q3. Does SF have its own sidecar process / pipe / WCF endpoint we are missing?
Strong evidence the answer is yes when SF is enabled:
aahClientCommon.CSFConnection.GetSFPipeName(token0x06004B72),GetSFPath(0x06004B71),IsConnected(0x06004B73),IsEnabled(0x06004B6F) — there is a separately-named SF Named Pipe distinct from the main MDAS pipe.aahClientCommon.CSFConnection.StartStoreforward(token0x06004BC6).IStorageServiceContractalready declaresGetStoreForwardParameter/SetStoreForwardParameter(GetSFP/SetSFP, seeIStorageServiceContract.cs:81-85) andStorageis a separate WCF service slot inHistorianWcfServiceNames.cs:15.CWcfConfig.ConfigurePipeProxy<IStorageServiceContract>(token0x06004B1C) andCWcfConfig.ConfigureTcpProxy<IStorageServiceContract>(token0x06004B1B) confirm the storage proxy supports both transports — same dual-transport pattern the History/Retrieval proxies use.CStorageEngineConsoleClient.GetPipeNameStr(token0x06000E2D) /GetFullPipeNameStr(token0x06000E2E) wraps the storage-engine console pipe viaSTransactPipeClient2(a non-WCF binary pipe protocol).
Open: is the SF sidecar even running on the dev host this SDK is being
tested against? handoff.md does not record an SF process being
observed. aveva-install-x64/ and aveva-install-x86/ ship only DLLs
(no aahStoreForwardClient.exe / aahSFClient.exe / similar). The SF
sidecar is part of the Historian server install, not the client
redistributable. So:
- On the developer machine, SF is reachable only because the local Historian server is installed.
- A pure-client install (the deployment target this SDK ships into) may never have SF.
This shapes the success criteria: when SF is not configured, a correct
implementation returns Pending = false, ErrorOccurred = false,
DataStored = false, Storing = false — i.e. the same shape the
synthesized defaults produce today. The interesting case is when SF is
configured and active.
Q4. Is SF state authoritative on the Historian server or on a per-client basis?
Native wrapper reads it from HistorianClient* (the per-connection C++
object). This means it is connection-scoped server-pushed state. We
do not need to enumerate cluster-wide SF state — the server reports
"my SF buffer for this client's writes" only. This matches our read-only
mission: we are not a writer, so the only SF state of interest is the
server-side cache for other writers, which the server can report to
us as a passive observer.
Q5. Does any SF probe require Admin?
CSFConnection.GetSFPipeName returns a kernel object name. Reading
from it requires the pipe ACL to permit the caller. If the SF pipe is
ACL'd to LocalSystem only, the SDK cannot read it without
impersonation — and the SDK runs as the calling process. This is a
hard limit, not a bug.
3. Discovery Workstreams
Run these in parallel. None require a live server beyond what the existing test rig already has.
Workstream A — Static IL inspection (parallel-safe, read-only)
Owner action items, in order:
- Dump full IL of token
0x06006187(HistorianAccess.GetStoreForwardStatus(ConnectionIndex,out)):Save underdotnet run --no-build --project tools\AVEVA.Historian.ReverseEngineering -- ` dnlib-method current\aahClientManaged.dll HistorianAccess.GetStoreForwardStatus --instructionsdocs/reverse-engineering/historianaccess-getstoreforwardstatus-il-latest.txt. Confirm thecallitarget signatureINSQL_MDAS_ERROR(IntPtr,uint,HISTORIAN_STORAGE_STATUS*)and that the only WCF entry-points it touches are zero. - Dump IL of
HistorianAccessUtil.ConvertUnmanagedSFStorageStatusToManagedStorageStatus(token0x060060E4). This is the unmanaged→managed mapping; it tells us which fields ofHISTORIAN_STORAGE_STATUSpopulate which fields ofHistorianStoreForwardStatus. We will need the same mapping in reverse on the wire response. - Inventory every method that writes into the local SF status
struct:
The known set as of writing:
methods current\aahClientManaged.dll SetStoreForward methods current\aahClientManaged.dll SetMdasStoreForwardCConfigStatusClient.SetMdasStoreForwardEvent(0x060029DC),aahClientCommon.CStatus.SetStoreForwardEvent(0x06002A04),CStatusConnectionDirect.SetStoreForwardEvent(0x06004DF8),CStatusConnectionWCF.SetStoreForwardEvent(0x06004E4E),CClientCommon.SetStoreForwardEventOnServer(0x06002EC0). TheWCFvariant is the one whose IL maps ontoIStatusServiceContract2.SetStoreForwardEvent(token0x06005F57) — read its IL and document the request/response shape. - Dump IL of
IStatusServiceContract2.SetStoreForwardEvent(0x06005F57) parameter types. The[OperationContract]declaration in the wrapper assembly already encodes the wire shape; this gives us the bytes the server pushes us.
Workstream B — Install inventory (parallel-safe)
- Inventory
aveva-install-x64\andaveva-install-x86\for any binary whose name containsStore,Forward,SF,Cache,Spool. As of this checkout: none, only DLLs. Confirm. - Inventory the deployed Historian server (out-of-band; not in this
repo) for
aahStoreForwardClient.exe,aahStoreForwardServer.exe,aahSFCache.exe, or any service registered withDescriptionmatching*Forward*. Capture the service name, account identity, and pipe ACLs (accesschk -wuvc). - Walk the registry:
HKLM\SOFTWARE\ArchestrA\Historianand any sub-key matching*StoreForward*, recording paths and pipe names. Sanitize before committing.
Workstream C — WCF probe (parallel-safe)
Use the existing wcf-probe and wcf-status subcommands of
tools\AVEVA.Historian.ReverseEngineering:
wcf-probe $env:HISTORIAN_HOST 32568— confirmStorage/GetVis reachable. (It is the third service slot inHistorianWcfServiceNames.) Document the returned interface version.wcf-status $env:HISTORIAN_HOST 32568 <param-name>— sweep plausible SF parameter names (SF.Status,StoreForward.State,SFCacheBytes, etc.) throughGetSystemParameterand record what the server accepts. Cheap, read-only, no session needed beyond the already-decoded auth chain.- Probe
GetHistorianInfo(GETHI,IStatusServiceContract2.cs:24) with the byte request shape used by the native wrapper. The request bytes are visible if we runinstrument-wcf-readquery-style instrumentation againstCConfigStatusClient.SetMdasStoreForwardEvent's upstream caller — see Workstream D.
Workstream D — Native capture (sequential after A and C)
Two captures are needed:
- Native call to
mdas_GetStorageStatus. Runtools\AVEVA.Historian.NativeTraceHarnesswith a new scenario--scenario sfstatus(to be added) that invokesHistorianAccess.GetStoreForwardStatus()and dumps theHISTORIAN_STORAGE_STATUSC struct memory before the managed conversion runs. This pins the binary layout of the struct (offsets, field widths, endianness) without us guessing. - WCF push of SF events. Configure the local Historian to enter
SF mode (stop the runtime DB writer; let the writer's queue
trigger SF) and capture the WCF traffic with the existing
instrument-wcf-readquerysibling — i.e. add aninstrument-wcf-setstoreforwardeventsubcommand that IL-rewritesaahClientManaged.dllto log the bytes the server sends toIStatusServiceContract2.SetStoreForwardEvent. Save the rewrite underdocs/reverse-engineering/dnlib-write-copy/, nevercurrent/.
Workstream D is the only step that needs an actively-storing SF sidecar. Plan: stop the Historian Runtime DB SQL service, write a single test point via the wrapper's writer harness, and capture the SF event push, then restart Runtime DB and capture the "end-of-SF / data drained" push.
Workstream E — On-disk cache (only if Workstream D fails)
If the WCF push protocol turns out to be impractical to reproduce (e.g. requires duplex contract, callback channel, or a server-side session-bind we cannot match from our managed client), fall back to inspecting the on-disk SF cache directly. Steps:
- Resolve
CSFConnection.GetSFPathIL to find the cache directory convention (likely%ProgramData%\ArchestrA\Historian\Cache\or similar — to be confirmed, never assume the path). - Inventory file types:
.sfdata,.sfindex,.cache— whatever the directory contains. - Decode the file header. The presence/size of
.sfdatafiles is sufficient to populateDataStoredandPending; we do not need to decode the value payload.
This fallback is only for DataStored / Pending. Storing and
Error fundamentally require a live server-state read.
4. Concrete Reverse-Engineering Steps (execution order)
Mirrors the read/event decoding workflow that succeeded for raw queries.
Step 1 — Find native methods that touch SF
Already done; baseline evidence is recorded in §2 Q1/Q3 above. Key tokens to reference:
0x06006186,0x06006187— public/privateHistorianAccess.GetStoreForwardStatus0x060060E4—HistorianAccessUtil.ConvertUnmanagedSFStorageStatusToManagedStorageStatus0x060029DC—CConfigStatusClient.SetMdasStoreForwardEvent0x06002A04—aahClientCommon.CStatus.SetStoreForwardEvent0x06002DFF—aahClientCommon.CClientCommon.IsInStoreForward0x06002E18—aahClientCommon.CClientCommon.SetStoreForwardParams0x06002EC0—CClientCommon.SetStoreForwardEventOnServer0x06004BC6—aahClientCommon.CSFConnection.StartStoreforward0x06004B6F..0x06004B73— CSFConnection getters (path, pipe, enabled, connected)0x06004DF8,0x06004E4E— direct vs WCF status connections0x06005F57—IStatusServiceContract2.SetStoreForwardEventMD ref0x06006193—HistorianAccess.IsBothConnectionRequested(used by the public arity-0 GetStoreForwardStatus to decide whether to fan out to a redundant partner)
Step 2 — Decode HISTORIAN_STORAGE_STATUS layout
Run Workstream A.2 (decode token 0x060060E4) and Workstream D.1
(native struct memory dump). Together they pin the field layout.
The managed struct fields we already know we need to populate
(from HistorianStoreForwardStatus.cs):
ServerName, Pending, ErrorOccurred, Error, DataStored,
Storing, ConnectionKind. The native struct will have ≥7
fields plus padding. Express the mapping as a comment table in
the implementation.
Step 3 — Decide the wire model
Two possible implementations:
- Push-mode (native parity). SDK opens an authenticated WCF
session that the server treats as a status subscriber, listens
for
IStatusServiceContract2.SetStoreForwardEventcallbacks, maintains a local cache, andGetStoreForwardStatusAsyncreturns from the cache. This requires WCF duplex (CallbackContract) which is not currently exercised anywhere insrc/AVEVA.Historian.Client/Wcf/. - Pull-mode (probe). SDK calls
GetHistorianInfo(GETHI) or a discoveredStorage-service equivalent and maps the one-shot response. No subscription state required.
Pull-mode is strongly preferred: it matches the SDK's existing
WCF style, avoids duplex contracts, and the existing code path
in HistorianWcfStatusClient.GetSystemParameter is the right
shape. Only fall back to push-mode if Workstream C.3 proves the
server has no pull endpoint that returns SF state.
Step 4 — Implement the managed contract method
Once Step 3 picks pull-mode, implement against the WCF contract
(likely a new [OperationContract] on IStatusServiceContract2
or a method on IStorageServiceContract). Follow the existing
parameter-naming discipline from the resolved
ValidateClientCredential blocker:
use [MessageParameter(Name = "...")] to match exact server
element names — do not let WCF derive them from C# parameter
names. See handoff.md "Active Blocker" entry for the
2026-05-04 fix.
Step 5 — Add golden-byte fixtures
Add a request and response fixture under
fixtures/protocol/store-forward-status/:
request-get-storage-status.bin— bytes the SDK sends.response-get-storage-status-running-normal.bin— server not in SF.response-get-storage-status-active-sf.bin— server actively storing.response-get-storage-status-error.bin— server's SF errored.
Capture sources: the same instrumented native wrapper runs that populate Workstream D. Sanitize hostnames, GUIDs, and timestamps before committing.
Step 6 — Replace the synthesized stub
Replace SynthesizeStoreForwardStatus (lines 107-117 of
HistorianWcfStatusClient.cs) with a real implementation. Keep
the synthesized fallback for the case where the storage service
returns a "no SF configured" sentinel — that is not an error
condition, it is the normal state for client-only deployments.
Add a unit test class WcfStoreForwardStatusProtocolTests next
to the existing WcfDataQueryProtocolTests etc., with golden-byte
parse tests using the fixtures from Step 5.
Update the operation status table in README.md:20 from
"synthesized defaults (no SF sidecar to probe)" to
"live-verified" once the integration test passes.
5. Risks and Gotchas
- SF may not be present on the test host. The dev Historian probably has SF disabled by default; turning it on means stopping Runtime DB SQL services, which is invasive. Plan to do capture work on a dedicated sacrificial Historian VM, not the shared dev box.
- SF sidecar may require Admin or LocalSystem to query. Any
pipe-direct fallback (Workstream E) will fail under standard
user accounts. Document the privilege requirement explicitly
in the SDK XML doc comments on
GetStoreForwardStatusAsync. - State is volatile. Probes that take >100 ms can race against the server's own SF state machine. Capture both request and response in the same instrumented run; do not try to correlate two captures.
- Push-mode would force a duplex WCF contract. None of the existing decoded operations use duplex. Adding it widens the managed WCF surface significantly and risks .NET-WCF compatibility issues we have not yet hit. Pull-mode first.
- The wrapper's
IsBothConnectionRequested(token0x06006193) path indicates a "primary + partner" topology. Out of scope for this pass per §1, but if the server returns partner data in the same response we must skip-decode (not throw on) unknown trailing bytes. Open2-only sessions never receive SF events.handoff.md"Active Blocker" notes the wrapper's full chain (OpenConnection3after theValClrounds) is the path that produces a session the server treats as a real client. SF probes must run from inside that chain — re-usingHistorianWcfAuthChainHelper.OpenAuthenticatedConnection, the same call site already used byGetSystemParameteratHistorianWcfStatusClient.cs:42.HISTORIAN_STORAGE_STATUSfield order is not contractual. The struct is C++ inside the closed source. If AVEVA reorders fields between Historian versions, our decoder breaks. Pin the decoder to the Historian server version observed at session open (already exposed viaIRetrievalServiceContractN) and reject mismatched versions explicitly withProtocolEvidenceMissingException. Do not silently best-effort parse.- Sanitization. Pipe names, registry paths, and SF cache
directory paths can leak hostnames and account names. Run the
rgsanitizer (handoff.md "Next Pickup Steps") after every doc edit.
6. Success Criteria
A real implementation is "done" when all of the following hold:
client.GetStoreForwardStatusAsync()returnsPending = trueandStoring = truewhile the local Historian's SF cache is actively buffering writes (verifiable by stopping the Runtime DB and writing a value).- Returns
Pending = falseandStoring = falsewithin ≤ 5 seconds after the Runtime DB recovers and SF drains. - Returns
ErrorOccurred = trueand a non-null, actionableErrormessage when the SF cache itself fails (disk full, pipe closed, etc.). - Returns the synthesized "no SF" shape (all-false) without throwing on a Historian where SF is not configured.
- Two new golden-byte unit tests pass (active-SF and idle-SF responses).
ProtocolGuardrailTestsno longer needs to exemptGetStoreForwardStatusAsyncfrom any "must throwProtocolEvidenceMissingException" rule — the method is now evidence-backed.- Live integration test
HistorianClientIntegrationTests.GetStoreForwardStatusAsync_ReturnsServerState(to be added) passes whenHISTORIAN_HOSTis set, skips cleanly otherwise. README.md:20operation status table is updated from "synthesized defaults" to "live-verified".
7. Open Questions for the Implementer
Resolve these before writing production code:
- Does the server expose a pull endpoint that returns the full
HISTORIAN_STORAGE_STATUSsnapshot, or only push events? (Workstream C.3 answers this.) - What is the binary layout of
HISTORIAN_STORAGE_STATUS? (Workstream A.2 + D.1.) - What is the
[OperationContract]shape onIStatusServiceContract2.SetStoreForwardEvent? Specifically: parameter count, byte-buffer parameters, and exactMessageParameternames? (Workstream A.4.) - Is the
Storageservice slot atnet.pipe://<host>/Storageandnet.tcp://<host>:32568/Storagereachable on a non-Historian-server install? Or does it 404 when only the client redistributable is present? (Workstream B + C.1.) - Does the SF status snapshot include partner / redundant SF
state inline, or is it returned from a separate call?
(Workstream A.1, look for branches under
IsBothConnectionRequested.) - Does the SF status read require
OpenConnection3to have succeeded, or isOpen2enough? (Trial: try the discovered pull endpoint afterOpen2only, before doingOpenConnection3. If it works, the implementation is much simpler.) - What happens when SF is disabled by configuration vs
enabled but idle? Both should map to
Pending=false, Storing=false, but the underlying server response may be a sentinel error vs an all-zeros struct. The implementation must distinguish "no SF" (return defaults silently) from "SF errored" (returnErrorOccurred = true).
8. Out of Scope
Explicitly not part of this plan:
- SF write-back (the project mission is read-only;
IStorageServiceContract.AddStreamValuesetc. stay unimplemented). - Setting SF parameters
(
IStorageServiceContract.SetStoreForwardParameter). - Redundant-partner SF aggregation
(
HistorianStoreForwardStatus.AddPartnerStoreForwardStatus). - Reverse-engineering the on-disk SF cache file format beyond presence / file count (Workstream E is a fallback, not a primary deliverable).
- Anything in the
aahClientCommon.CSFConnection.StartStoreforward/SetStorageStopped/SetTagSynchronizedwrite surface.
9. 2026-06-21 gRPC re-scope (current R4.3 plan)
This supersedes the recommended route in §2/§3/§4. The deliverable (§1) and success criteria (§6) are unchanged. What changed is the transport and the resolved architecture risk.
9.1 What the recovered gRPC contract already gives us
The 2023 R2 contract under src/AVEVA.Historian.Client/Grpc/Protos/
exposes SF state through first-class pull RPCs on StorageService
(StorageService.proto) — no duplex/callback contract, no native
HISTORIAN_STORAGE_STATUS C-struct decode:
GetSFParameter(uint32 Handle, string ParameterName) → (Status status, string ParamaterValue)— the direct analogue of the already-shippedGetSystemParameter/GetRuntimeParameterstring-keyed pulls. This is the primary SF-state lever: a name→value read.GetRemainingSnapshotsSize(uint32 Handle) → (Status status, uint64 SnapshotSize)— the pending-buffer magnitude in one call. Non-zero ⇒ data is queued (Pending/DataStored=true); zero ⇒ drained. The cleanest single signal for the idle-vs-active split.GetInfo(string Request) → (Status status, bytes info)— generic server info blob; a fallback if a named SF key lives here instead of inGetSFParameter.OpenStorageConnectionResponse.ServerStatus(field 5) and theGetSnapshots/StartQuerySnapshotfamily — secondary signals.
SetSFParameter exists too but is out of scope (read-only mission, §8).
The TransactionService.ForwardSnapshot{,Begin,End} RPCs are the SF
cache replay/transfer path (write-side), not a status read — also
out of scope here; they belong to the deferred bit-faithful SF cache work,
not to GetStoreForwardStatusAsync.
9.2 Plumbing that already exists (reuse, don't rebuild)
HistorianGrpcHandshake.OpenSession— authenticated gRPC session (ValidateClientCredentialNTLM loop + Open2) yieldingClientHandle(uint) + storage-session GUID. Live-verified against the 2023 R2 box.HistorianGrpcStorageConnectionProbe— already constructs aStorageService.StorageServiceClient, primesGetInterfaceVersion, and callsOpenStorageConnection/CloseStorageConnection. The SF-status probe is a near-clone that swaps theOpenStorageConnectionbody forGetSFParameter/GetRemainingSnapshotsSizecalls.HistorianGrpcChannelFactory/HistorianGrpcConnection— channel, metadata, deadlines.
9.3 The one open risk that survives: which Handle?
GetSFParameter/GetRemainingSnapshotsSize both take uint32 Handle.
Unknown: do they accept the session ClientHandle (from
OpenSession, which is cheap and unblocked), or do they require the
storage console Handle returned by OpenStorageConnection — which
is the D2 wall (OpenStorageConnection routes to the
\\.\pipe\aahStorageEngine\console session and is the same storage-engine
pipe that blocks revision writes)? See
project_roadmap_exhausted_2020wcf and HistorianGrpcStorageConnectionProbe
header.
- Best case: these read-only status RPCs accept the session
ClientHandle(status reads shouldn't need a console writer session). Then R4.3-over-gRPC is unblocked end-to-end and is a small, shippable feature. - Worst case: they require the
OpenStorageConnectionHandle⇒ R4.3 inherits the D2 storage-engine-pipe wall and stays blocked on the same root cause as R4.2. Either way the probe answers it in one run.
9.4 Discovery steps (execution order)
- Add
grpc-sf-status-probetotools/AVEVA.Historian.ReverseEngineering(mirrorHistorianGrpcStorageConnectionProbe). Against the live 2023 R2 server it:- opens an authenticated session, gets
ClientHandle; - calls
GetRemainingSnapshotsSize(ClientHandle)and reportsstatus.bSuccess+SnapshotSize+ any error buffer; - sweeps
GetSFParameter(ClientHandle, name)over a candidate name list (Status,Storing,Pending,DataStored,SF.Status,StoreForwardStatus,Forward,CacheSize,ErrorOccurred, plus any names surfaced by Workstream A's IL ofConvertUnmanagedSFStorageStatusToManagedStorageStatus); - records which names the server accepts and the returned values.
- If every call fails with an auth/handle-shaped error, retry once
with the
OpenStorageConnectionHandleto disambiguate §9.3.
- opens an authenticated session, gets
- Idle baseline first — run against the server with SF not active.
Establishes the "no SF / drained" response shape (expected:
SnapshotSize=0, parameter reads succeed-with-defaults or return a "not configured" sentinel). This alone may be enough to ship an honest idle-state implementation that is strictly better than today's hardcoded all-false synthesis (it would be measured false). - Active-SF capture — only if step 2 proves the read works and we
need the active-state fixtures. Force SF on the sacrificial Historian
VM (stop Runtime DB writer; let the queue spill to SF), re-run the
probe, capture the non-zero/
Storing=trueresponse. This is the one invasive step and the gate on full success criteria §6.1–6.3. - Map + implement — add
GrpcGetStoreForwardStatusto the gRPC read orchestrator, map the probed fields ontoHistorianStoreForwardStatus, routeGetStoreForwardStatusAsyncto it whenTransport == RemoteGrpc(keep the synthesized fallback for non-gRPC transports and for the "no SF configured" sentinel). Add golden-byte fixtures (idle + active) andWcfStoreForwardStatusProtocolTests-style parse tests. Gate the live integration test onHISTORIAN_GRPC_HOST.
9.5 Effort / feasibility summary
- Risk collapsed: pull-vs-push (the old plan's worst risk) is settled — it's a pull. No duplex WCF/gRPC callback contract.
- No native struct decode:
GetSFParameterreturns a string; we skip theHISTORIAN_STORAGE_STATUSC-layout RE entirely (Workstream A.2 / D.1 become "nice-to-have for field names", not blocking). - Reuses shipped plumbing: session open +
StorageServiceClient+ channel already exist and are live-verified. - Remaining unknowns are empirical, one probe-run each: (a) the
accepted parameter-name vocabulary, (b) which
Handlethe status RPCs want (§9.3 — the only thing that could re-block it), (c) the active-SF response shape (needs the invasive force-SF step). - Net: Step 1–2 are low-risk and could land a measured idle-state
GetStoreForwardStatusAsyncover gRPC quickly. Steps 3–4 (full success criteria) still need the sacrificial-VM force-SF capture and are gated on §9.3 not landing on the D2 wall.
9.6 Out of scope (unchanged from §8, restated for gRPC)
SetSFParameter, ForwardSnapshot* (SF replay/transfer), the on-disk
cache file format, and redundant-partner SF aggregation all remain out of
scope. R4.3 is read-only status, gRPC-first.
9.7 Idle-baseline run — RESULTS (2026-06-21)
Built HistorianGrpcStoreForwardStatusProbe + the grpc-sf-status-probe
CLI command and ran it against the live 2023 R2 server with the
historian in its idle / not-actively-storing state (storage interface
v4, authenticated session opened OK). Tested both read-only (0x402) and
write-enabled (0x401) sessions. Findings, with the §9.3 handle question
resolved:
-
Direct
StorageServiceSF pull RPCs are D2-gated — confirmed the §9.3 worst-case branch.GetRemainingSnapshotsSize(session.ClientHandle)→bSuccess=false, error buffer04 84 00 00 00(= status0x84/ 132OperationNotEnabled). Identical under0x401and0x402— so it is NOT the read/write connection-mode gate; the History-sessionClientHandleis simply not a valid handle for this op's handle-space.GetSFParameter(session.ClientHandle, <name>)→ server-sideRpcException(Unknown, "Exception was thrown by handler")for all 16 candidate names, both session modes.- These two ops need the
OpenStorageConnectionconsole handle, andOpenStorageConnectionitself fails with the storage-engine console error (84 55 00 00 00 01 02 00 09 15 00- ASCII
"OpenStorageConnection") — the D2 storage-engine-pipe wall, the same root cause that blocks R4.2 revision writes. We cannot obtain the console handle, so these two SF RPCs are unreachable from a pure managed client. See project_roadmap_exhausted_2020wcf.
- ASCII
-
One reachable session-handle lever found:
StatusService.GetHistorianConsoleStatus(strHandle)SUCCEEDS with the session string handle (uppercase Open2 GUID) — no console handle needed — and returnsuiConsoleStatus = 3at idle. This is the only SF-adjacent signal reachable from the managed client. Its enum semantics are unknown (3 = presumably "running/normal"); whether it shifts when SF is actively storing is the open question. -
StatusService.GetHistorianInfo(strHandle, btRequest)→bSuccess= falsefor everybtRequestcandidate (empty /u32(0)/ ascii+utf16"StoreForward"); its request framing is not yet known. Lower-yield thanGetHistorianConsoleStatus; revisit only if needed.
Net idle-baseline conclusion. R4.3's clean direct route
(GetSFParameter / GetRemainingSnapshotsSize) is blocked behind the
D2 storage-engine console pipe, exactly like R4.2 — a pure managed
client cannot open the console session those ops require. The only
reachable SF-adjacent signal is GetHistorianConsoleStatus → a status
uint. Two paths forward:
- (a) Ship a measured idle-state only. — SHIPPED + LIVE-VERIFIED 2026-06-21.
HistorianGrpcStatusClient.GetStoreForwardStatusAsyncopens a session, callsGetHistorianConsoleStatus, and returnsHistorianStoreForwardStatusall-false but measured: it actually contacts the server and reportsErrorOccurred=true(with the underlying error) when the server is unreachable / the console-status call fails — strictly better than the blind hardcoded synthesis, which never contacts the server. Routed viaHistorian2020ProtocolDialect.GetStoreForwardStatusAsyncwhenTransport == RemoteGrpc(non-gRPC keeps the synthesized fallback). Gated live testHistorianGrpcIntegrationTests.GetStoreForwardStatusAsync_OverGrpc_ReturnsMeasuredIdleStatepasses against the real 2023 R2 server.Storing/Pending/DataStoredmagnitude is intentionally NOT surfaced — it lives behind the D2 wall (see path (b)). - (b) Full success criteria (§6) stay blocked on the D2 console-pipe
wall. Decoding the active-SF
uiConsoleStatusvalue and anyGetSystemParameterSF keys still needs the invasive force-SF capture on a sacrificial Historian — and even thenStoring/DataStoredmagnitude is only available via the D2-gatedGetRemainingSnapshotsSize.
Probe code: src/AVEVA.Historian.Client/Grpc/HistorianGrpcStoreForwardStatusProbe.cs,
CLI grpc-sf-status-probe <host> [port] [--tls] [--dnsid <n>] [--write-session].
Writes nothing; releases any console session immediately.