Skip to content

Streaming terminal for sensors and FRUs: SensorsRequest.stream() and FrusRequest.stream(), as in winrm-java #235

Closed

Description

@bertysentry

Need

bmc.sensors().execute() and bmc.frus().execute() return only after every sensor reading or FRU inventory has been read. On a large BMC that takes a while: hundreds of Get Sensor Reading round trips (each bridged sensor goes through Send Message, and a silent satellite controller costs a full timeout), and FRU reads of a few hundred bytes each in chunks of 16 to 255 bytes (#102). Until the operation ends, the caller sees nothing:

  • MetricsHub cannot start processing the first sensors while the last ones are being read, and the operation deadline must cover the whole walk;
  • ipmicli sensors / ipmicli fru print nothing until the end, then everything at once;
  • a caller that only needs the first few matching sensors (limit(n), findFirst()) still pays for all of them.

winrm-java solved the same problem with a streaming terminal next to the blocking one (WqlRequest.stream(), RemoteDirectoryListing.stream()). ipmi-java should offer the same, with the same contract, so the two MetricsHub libraries feel alike.

Proposed API

try (Stream<Sensor> sensors = bmc.sensors().types(SensorType.Temperature).stream()) {
	sensors.forEach(sensor -> process(sensor));
}

try (Stream<Fru> frus = bmc.frus().stream()) {
	frus.filter(fru -> fru.serialNumber() != null).forEach(this::inventory);
}
  • SensorsRequest.stream() and FrusRequest.stream(): a lazy, sequential, ordered Stream (Java 8 StreamSupport over a Spliterator, as in winrm-java). Each Sensor / Fru is yielded as soon as it is read, in the same order as execute(). The selection (types(), names(), ids()) applies as it does today: an unselected sensor costs no round trip.
  • execute() stays and keeps its result type; it can become stream().collect(...) internally so both paths share one implementation (as RemoteDirectoryListing.execute() does).
  • The stream must be closed (same contract as Files.lines() and winrm-java): try-with-resources. Closing early stops the walk; no further command is sent.
  • The SDR records skipped by the walk (SensorList.skippedRecords() / FruList.skippedRecords()) must stay reachable: the SDR repository is walked before the first element, so they are known when stream() returns. Options: an onSkippedRecord(Consumer<SkippedRecord>) on the request (like winrm-java's RemoteDirectoryListing.onInaccessible()), or a small result type holding both the stream and the skipped records. Pick one; SkippedRecord must not be lost silently (AGENTS.md: the library never logs, the caller gets the problems).
  • Per-sensor and per-FRU problems are unchanged: they stay in Sensor.problem() / Fru.problems() and do not end the stream. A failure that ends the walk (session lost, BMC stops answering) is thrown from the stream's operation (forEach, tryAdvance...), as in winrm-java; stream() itself throws when the session cannot be opened or the SDR repository cannot be read.

Points to settle in the implementation

Things in IpmiClient that a streaming terminal does not fit as-is:

  1. Single worker thread and deadline. Every operation goes through run(): a task submitted to the client's single worker, awaited with future.get(deadline). A stream is consumed on the caller's thread, element by element, so the deadline cannot be the duration of the whole operation (the caller's processing time would count). Follow winrm-java: the deadline becomes an inactivity timeout, the longest wait for the next element (or for the SDR walk before the first one). Document that difference in the Javadoc of stream() and of timeout(Duration).
  2. Serialization. While a stream is open, it owns the session: other operations on the same client (and the keep-alive) must wait until the stream is closed or exhausted, or be interleaved safely between two elements. winrm-java blocks them ("other operations on the same client block until it is closed or exhausted"); say which one ipmi-java does, and make sure an abandoned, unclosed stream cannot block the client forever (at least IpmiClient.close() must still work).
  3. Replay after a lost session. perform() reruns the whole task once when the session is lost (replayable). A stream that has already emitted elements must not replay from the start (duplicates). Either resume after the last element emitted (re-open the session, keep the SDR records already walked, continue with the next sensor/FRU), or end the stream with the IpmiConnectionException/IpmiTimeoutException. Resuming is better for MetricsHub; document the choice.
  4. Silent satellite controllers. readSensors() keeps a silentOwners set across the walk so that a controller that did not answer is not waited for again: it must live in the stream's state, not be rebuilt per element.
  5. SDR cache. The walk uses repository() (shared with frus() while the cache is current): unchanged, the walk happens before the first element.

CLI

ipmicli sensors and ipmicli fru should use the stream, so that each row (and each --json line) is printed as it is read. The table output needs its column widths before the first row: either keep collecting for the table and stream only --json / JSON Lines output, or switch the table to fixed widths. Golden files must stay byte-identical for the collected output.

Out of scope / later

  • SEL: bmc.sel() could get the same stream() (entries are read one Get SEL Entry at a time), but SEL reservation handling (Expose SEL (System Event Log) reading in the client API #103) makes it a separate change. File it separately if wanted.
  • No reactive/async API (CompletableFuture, Flow): a plain java.util.stream.Stream, Java 8, like winrm-java.

Done when

  • SensorsRequest.stream() and FrusRequest.stream() exist, with Javadoc stating the close contract, the inactivity timeout, which exceptions come from stream() and which from the stream's operations, and how skipped SDR records are reported.
  • execute() returns the same results as today (fake-BMC tests unchanged).
  • Unit tests against the fake BMC: elements arrive before the walk ends; early close() sends no more commands; limit(n) reads only n sensors; a session lost mid-stream behaves as documented; an unselected sensor costs no command.
  • README.md, src/site/markdown (sensors and FRU pages) and upgrading.md describe the new terminal.

Activity

  1. self-assigned this
    on Oct 27, 2026
  2. added a commit that references this issue on Oct 27, 2026
    f4ff313
  3. added a commit that references this issue on Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions