|
1 | 1 | # StatusServer |
2 | 2 |
|
| 3 | +A non-disturbing data collector for the [X-Environment](http://www.github.com/waltz-controls/xenv) (Integrated Control System for High-throughput Tomography). It aggregates control system data from Tango and/or TINE servers, tracks attribute availability, persists downtime intervals to MariaDB, and exposes live metrics via HTTP/Prometheus. |
3 | 4 |
|
4 | | -[](https://codecov.io/gh/hzg-wpi/status-server) |
5 | | -[](https://codebeat.co/projects/github-com-xenvhzg-status-server-master) |
| 5 | +## Requirements |
6 | 6 |
|
7 | | -[](https://github.com/hzg-wpi/status-server/releases/latest) |
| 7 | +- Java 21 |
| 8 | +- Maven 3.8+ |
| 9 | +- Tango 9+ (for Tango attributes) / TINE (for TINE attributes) |
| 10 | +- MariaDB 11 (optional — required for downtime persistence) |
8 | 11 |
|
9 | | -This project is a part of [X-Environment](http://www.github.com/waltz-controls/xenv) (Integrated Control System for High-throughput Tomography experiments). X-Environment is a bunch of components that server two main goals: |
| 12 | +## Quick Start |
10 | 13 |
|
11 | | -* Collect data during the High throughput Tomography experiment in a non-disturbing way (does not disturb experiment) |
12 | | -* Provide high level abstraction for beamline scientist to control the experiment |
| 14 | +```bash |
| 15 | +# Build fat JAR |
| 16 | +mvn clean package -Dmaven.test.skip=true |
13 | 17 |
|
14 | | -This Tango server corresponds to the first goal. It aggregates data from upstream [Tango](http://www.tango-controls.org) and/or [TINE](http://adweb.desy.de/mcs/tine/) servers it acts as an intermediate buffer for the collected data. |
| 18 | +# Start MariaDB (and optional Tango stack) |
| 19 | +docker compose up -d status-server-db |
15 | 20 |
|
16 | | -# Requirements |
17 | | - |
18 | | -* Java 11 server environment |
19 | | -* Tango 9+ |
| 21 | +# Run |
| 22 | +java -jar target/status-server-*.jar path/to/config.xml [http-port] |
| 23 | +# http-port defaults to 9190 |
| 24 | +``` |
20 | 25 |
|
21 | | -# User guide |
| 26 | +## Configuration |
22 | 27 |
|
23 | | -[](http://status-server.readthedocs.io/en/latest/?badge=latest) |
| 28 | +Configuration is an XML file. Minimal example: |
24 | 29 |
|
25 | | -# How To ... |
| 30 | +```xml |
| 31 | +<status-server stale-after="3" down-after="6"> |
| 32 | + <devices> |
| 33 | + <device name="my/device/1" server="tango://host:10000"> |
| 34 | + <attributes> |
| 35 | + <attribute name="Temperature" poll-delay="1000" interpolation="LINEAR"/> |
| 36 | + </attributes> |
| 37 | + </device> |
| 38 | + </devices> |
| 39 | +</status-server> |
| 40 | +``` |
26 | 41 |
|
27 | | -## ... resolve dependencies (ver > 1.0.0) |
28 | | -goto {SS_ROOT}/devkit and execute install.bat(.sh) |
| 42 | +Optional MariaDB section (omit to disable persistence): |
29 | 43 |
|
30 | | -for older versions manually download and install tine and TangoAPI jars. |
| 44 | +```xml |
| 45 | +<mariadb> |
| 46 | + <jdbc-url>jdbc:mariadb://localhost:3306/statusserver</jdbc-url> |
| 47 | + <user>ss</user> |
| 48 | + <password>ss</password> |
| 49 | +</mariadb> |
| 50 | +``` |
31 | 51 |
|
32 | | -## ... build from source |
| 52 | +| Parameter | Description | Default | |
| 53 | +|---|---|---| |
| 54 | +| `stale-after` | Consecutive failures before UP→STALE | 3 | |
| 55 | +| `down-after` | Consecutive failures before STALE→DOWN | 6 | |
33 | 56 |
|
34 | | -There are still some tests that are strictly environment depended (e.g. TangoClientTest) to build the project simply turn off tests in maven: ```mvn package -Dmaven.test.skip=true``` |
| 57 | +## Architecture |
35 | 58 |
|
36 | | -## ... run StatusServer without Tango environment (ver < 1.0.0) |
| 59 | +``` |
| 60 | +DeviceSource (XML) |
| 61 | + │ |
| 62 | + ▼ |
| 63 | +EngineFactory ──► Engine |
| 64 | + │ |
| 65 | + ┌─────────┴──────────┐ |
| 66 | + ▼ ▼ |
| 67 | + PollTask EventTask |
| 68 | + │ │ |
| 69 | + ▼ ▼ |
| 70 | + EventSink<SingleRecord<?>> EventSink<TechnicalEvent> |
| 71 | + (telemetry) (AvailabilityAnalyzer) |
| 72 | + │ │ |
| 73 | + ▼ ▼ |
| 74 | + InMemoryWriter EventSink<DomainEvent> |
| 75 | + (Snapshot only) (EventDispatcher fan-out) |
| 76 | + │ │ |
| 77 | + ▼ ┌────┴────┐ |
| 78 | + MetricsServer logger MariaDbSink |
| 79 | + (HTTP /metrics) |
| 80 | +``` |
37 | 81 |
|
38 | | -As the for version 1.8 there is no way to run StatusServer without Tango installed. At least programmatically. But it is possible to adjust code so it will start: |
| 82 | +### Key components |
39 | 83 |
|
40 | | -1. change the code of the Launcher.java (see below) |
41 | | -2. change the code of the StatusServerClass.java (see below) |
42 | | -3. execute {{{mvn clean package}}} |
43 | | -4. deploy |
44 | | -5. run with additional JVM parameter: -DOAPort=56234 (see below full command) |
| 84 | +**`EventSink<T>`** — unified observer interface replacing the old `RecordWriter` and `TechnicalEventListener`. Everything that consumes events implements this single generic interface. |
45 | 85 |
|
46 | | -In Launcher.java change main method: |
| 86 | +**`EventDispatcher<T>`** — fan-out dispatcher. Calls all registered sinks, isolates failures per sink. |
47 | 87 |
|
48 | | -``` |
49 | | -#!java |
50 | | -Launcher.java |
51 | | -public static void main(String[] args) { |
52 | | -... |
53 | | -log.info("Initializing Tango framework..."); |
54 | | -Util util = Util.init(new String[]{configuration.getInstanceName(),"-nodb","-dlist","development/local/0"}, configuration.getServerName()); |
55 | | -util.add_class(configuration.getServerName()); |
56 | | -Util.set_serial_model(TangoConst.NO_SYNC); |
57 | | -log.info("Done."); |
58 | | -... |
59 | | -} |
60 | | -``` |
| 88 | +**`AvailabilityAnalyzer`** — consumes `TechnicalEvent`s, maintains per-attribute state machines (UP/STALE/DOWN), emits `DomainEvent`s. |
61 | 89 |
|
62 | | -In StatusServerClass.java comment ```write_class_property``` and ```get_class_property``` methods call in the constructor (```StatusServerClass(String s)```) |
| 90 | +**`AttributeAvailability`** — per-attribute state machine: |
63 | 91 |
|
64 | 92 | ``` |
65 | | -#!java |
66 | | -StatusServerClass.java |
67 | | -... |
68 | | -protected StatusServerClass(String s) throws DevFailed { |
69 | | - super(s); |
70 | | -
|
71 | | -// write_class_property(); |
72 | | -// get_class_property(); |
73 | | -} |
74 | | -... |
| 93 | +consecutive failures ≥ stale-after → UP → STALE |
| 94 | +consecutive failures ≥ down-after → STALE → DOWN (+ DowntimeOpened) |
| 95 | +any success → any → UP (+ DowntimeClosed if from DOWN) |
75 | 96 | ``` |
76 | 97 |
|
77 | | -To run StatusServer use the following command: |
78 | | -``` |
79 | | -java -DXmx1G -Dss.home={SS_HOME} -DOAPort=56234 wpn.hdri.ss.Launcher --config {PATH_TO_CONFIG} |
80 | | -``` |
| 98 | +**`InMemoryWriter`** — snapshot-only in-memory store backing the `/metrics` endpoint. |
81 | 99 |
|
82 | | -# Benchmark tests |
| 100 | +**`MariaDbSink`** — persists domain events to MariaDB in ERPNext-compatible tables. Reconnects automatically on failure. |
83 | 101 |
|
84 | | -## Server and client are running on the same machine. |
| 102 | +## HTTP Endpoints |
85 | 103 |
|
86 | | -Machine configuration: |
87 | | -* Intel(R) Core(TM) i5-2500 CPU @ 3.30GHz |
88 | | -* RAM 8Gb |
89 | | -* RAM&CPU clock //TODO |
90 | | -* java version "1.6.0_29" |
91 | | -* Java(TM) SE Runtime Environment (build 1.6.0_29-b11) |
92 | | -* Java HotSpot(TM) 64-Bit Server VM (build 20.4-b02, mixed mode) |
| 104 | +| Endpoint | Description | |
| 105 | +|---|---| |
| 106 | +| `GET /metrics` | Prometheus gauge format. Each attribute emits a value gauge and `_up` (1=healthy, 0=failing). | |
| 107 | +| `GET /health` | Liveness — always 200. | |
| 108 | +| `GET /ready` | Readiness — 503 until engine has started. | |
93 | 109 |
|
94 | | -The application started in -server mode |
| 110 | +## Availability Tracking & Downtime Persistence |
95 | 111 |
|
96 | | -100K getLatestSnapshot were performed to warm up the JVM |
| 112 | +StatusServer classifies each read outcome as a technical event: |
97 | 113 |
|
98 | | -Results measured on 10K invocations of getLatestSnapshot: |
| 114 | +| Event | Trigger | |
| 115 | +|---|---| |
| 116 | +| `ReadSuccess` | Successful attribute read | |
| 117 | +| `ReadFailure` | Client exception during read | |
| 118 | +| `Timeout` | Read timed out | |
| 119 | +| `Disconnect` / `Reconnect` | Connection lost / restored | |
99 | 120 |
|
100 | | -``` |
101 | | -Delta time in getLatestValues (nano) = 11746809173 |
102 | | -Delta time in getLatestValues (millis) = 11746 |
103 | | -Average time in getLatestValues (nano) = 1174680 |
104 | | -Average time in getLatestValues (millis) = 1 |
105 | | -Average time in getLatestValues (seconds) = 0 |
106 | | -``` |
| 121 | +The `AvailabilityAnalyzer` aggregates these per attribute and emits domain events when thresholds are crossed: |
107 | 122 |
|
108 | | -## Server and client are running on different machines connected with 1Gbit network. |
| 123 | +| Domain Event | Meaning | |
| 124 | +|---|---| |
| 125 | +| `AvailabilityTransitioned` | State changed (UP↔STALE↔DOWN) | |
| 126 | +| `DowntimeOpened` | Attribute entered DOWN state | |
| 127 | +| `DowntimeClosed` | Attribute recovered from DOWN | |
109 | 128 |
|
110 | | -Server machine configuration: |
| 129 | +### MariaDB Schema |
111 | 130 |
|
112 | | -* Intel(R) Xeon(R) CPU E5620 @ 2.40GHz |
113 | | -* RAM 48Gb |
114 | | -* RAM&CPU clock //TODO |
115 | | -* java version "1.6.0_33" |
116 | | -* Java(TM) SE Runtime Environment (build 1.6.0_33-b03) |
117 | | -* Java HotSpot(TM) 64-Bit Server VM (build 20.8-b03, mixed mode) |
| 131 | +Three ERPNext-compatible tables (standard `tab{DocType}` naming, standard audit columns): |
118 | 132 |
|
119 | | -The application started in -server mode. |
| 133 | +``` |
| 134 | +tabState Transition — full history of every state change |
| 135 | +tabCurrent State — one row per attribute, UPSERT on every transition |
| 136 | +tabDowntime Interval — one open row per active downtime, closed on recovery |
| 137 | +``` |
120 | 138 |
|
121 | | -100K getLatestSnapshot were performed to warm up the JVM |
| 139 | +Apply schema to a fresh database: |
122 | 140 |
|
123 | | -Results measured on 10K invocations of getLatestSnapshot: |
| 141 | +```bash |
| 142 | +# Fresh container (wipes existing data) |
| 143 | +docker compose down -v |
| 144 | +docker compose up -d status-server-db |
124 | 145 |
|
125 | | -``` |
126 | | -Delta time in getLatestValues (nano) = 71063988508 |
127 | | -Delta time in getLatestValues (millis) = 71063 |
128 | | -Average time in getLatestValues (nano) = 7106398 |
129 | | -Average time in getLatestValues (millis) = 7 |
130 | | -Average time in getLatestValues (seconds) = 0 |
| 146 | +# Or apply manually to a running container |
| 147 | +docker exec -i status-server-db mariadb -u ss -pss statusserver < db/schema.sql |
131 | 148 | ``` |
132 | 149 |
|
133 | | -Ping: |
134 | | -``` |
135 | | -Pinging 131.169.65.240 with 32 bytes of data: |
136 | | -Reply from 131.169.65.240: bytes=32 time<1ms TTL=64 |
137 | | -Reply from 131.169.65.240: bytes=32 time<1ms TTL=64 |
138 | | -Reply from 131.169.65.240: bytes=32 time<1ms TTL=64 |
139 | | -Reply from 131.169.65.240: bytes=32 time<1ms TTL=64 |
140 | | -
|
141 | | -Ping statistics for 131.169.65.240: |
142 | | - Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), |
143 | | -Approximate round trip times in milli-seconds: |
144 | | - Minimum = 0ms, Maximum = 0ms, Average = 0ms |
145 | | -``` |
| 150 | +On startup, if MariaDB is configured, StatusServer reads `tabCurrent State` and seeds the in-memory state machines — attributes that were DOWN when the server last stopped resume tracking correctly. |
146 | 151 |
|
| 152 | +## Development |
147 | 153 |
|
148 | | -## 1.0.10 Benchmark test: |
| 154 | +```bash |
| 155 | +# Build (skip environment-dependent tests) |
| 156 | +mvn clean package -Dmaven.test.skip=true |
149 | 157 |
|
150 | | -The test was performed on a single host. |
| 158 | +# Run unit tests (no Tango/TINE required) |
| 159 | +mvn test -Dtest="AvailabilityAnalyzerTest,StatusServerStatusServerConfigurationTest" |
151 | 160 |
|
| 161 | +# Run all tests (requires live Tango) |
| 162 | +mvn test |
152 | 163 | ``` |
153 | | -Result "test_getLatestSnapshot": |
154 | | - 1042.600 ±(99.9%) 2.616 ops/s [Average] |
155 | | - (min, avg, max) = (575.350, 1042.600, 1724.124), stdev = 74.460 |
156 | | - CI (99.9%): [1039.984, 1045.216] (assumes normal distribution) |
157 | 164 |
|
| 165 | +Safe unit tests (no external dependencies): `data2/`, `configuration/`, `engine2/AvailabilityAnalyzerTest`. |
158 | 166 |
|
159 | | -# Run complete. Total time: 02:36:29 |
| 167 | +## Architecture Decision Records |
160 | 168 |
|
161 | | -Benchmark Mode Cnt Score Error Units |
162 | | -tango.ss.benchmark.SimpleBenchmark.test_getLatestSnapshot thrpt 8780 1042.600 ± 2.616 ops/s |
163 | | -``` |
| 169 | +### ADR-1: XML-only device source |
164 | 170 |
|
165 | | -## 1.3.2 Benchmark test: |
| 171 | +Frappe/ERPNext is used as a configuration front-end. It exports device/attribute lists as XML files. StatusServer reads that XML directly. There is no runtime Frappe dependency — this removes the HTTP round-trip on startup and makes the server runnable without an ERPNext instance. |
166 | 172 |
|
| 173 | +### ADR-2: Unified `EventSink<T>` interface |
167 | 174 |
|
168 | | -One attribute 100K records |
| 175 | +`RecordWriter` (telemetry) and `TechnicalEventListener` (availability signals) were merged into a single `EventSink<T>` functional interface. Rationale: a writer *is* a listener — it reacts to events. The generic parameter carries the event type, keeping the type system honest while eliminating the duplicate observer hierarchies. |
169 | 176 |
|
170 | | -``` |
171 | | -# Run complete. Total time: 00:13:55 |
| 177 | +### ADR-3: `EventDispatcher<T>` replaces `WriterDispatcher` |
172 | 178 |
|
173 | | -Benchmark Mode Cnt Score Error Units |
174 | | -SearchBenchmark.benchmarkLatest thrpt 200 8751053,333 ? 32440,027 ops/s |
175 | | -SearchBenchmark.benchmarkSnapshot thrpt 200 3256768,953 ? 14576,755 ops/s |
176 | | -SearchBenchmark.benchmarkRange thrpt 200 1290584,274 ? 19983,747 ops/s |
| 179 | +A single generic fan-out dispatcher replaces the telemetry-specific `WriterDispatcher`. Both the telemetry pipeline (`SingleRecord<?>`) and the domain event pipeline (`DomainEvent`) use the same implementation. Failures in one sink are logged and isolated; other sinks always receive the event. |
177 | 180 |
|
178 | | -``` |
| 181 | +### ADR-4: Snapshot-only in-memory store |
179 | 182 |
|
180 | | -100 attributes with 100K randomly distributed records |
| 183 | +`AllRecords` (full time-series history) was retired. The in-memory store now keeps only the latest value per attribute (`Snapshot`). Rationale: historical queries are served by MariaDB; keeping a second growing in-memory copy adds memory pressure with no remaining consumer. |
181 | 184 |
|
182 | | -``` |
183 | | -# Run complete. Total time: 00:27:19 |
| 185 | +### ADR-5: Global availability thresholds |
184 | 186 |
|
185 | | -Benchmark Mode Cnt Score Error Units |
186 | | -SearchBenchmark.benchmarkLatest thrpt 200 120810,181 ? 884,829 ops/s |
187 | | -SearchBenchmark.benchmarkRange thrpt 200 54811,091 ? 875,401 ops/s |
188 | | -SearchBenchmark.benchmarkSnapshot thrpt 200 36707,332 ? 228,565 ops/s |
189 | | -SearchBenchmark.benchmarkUpdates thrpt 200 36955,929 ? 196,897 ops/s |
190 | | -``` |
191 | | - |
192 | | -## 2.0.0 Benchmark test: |
| 187 | +`stale-after` and `down-after` are global values configured at the server level, not per attribute. Rationale: in this deployment all attributes are polled at similar rates; per-attribute thresholds add configuration complexity without practical benefit. |
193 | 188 |
|
194 | | -One attribute 100K records |
| 189 | +### ADR-6: ERPNext-compatible MariaDB schema |
195 | 190 |
|
196 | | -``` |
197 | | -# Run complete. Total time: 00:21:54 |
| 191 | +Tables follow ERPNext naming (`tab{DocType}`) and include the standard Frappe audit columns (`name`, `creation`, `modified`, `modified_by`, `owner`, `docstatus`, `idx`). Rationale: rows can be imported into or consumed by a Frappe/ERPNext instance without transformation, enabling ERP-level downtime reporting and billing workflows. |
198 | 192 |
|
199 | | -Benchmark Mode Cnt Score Error Units |
200 | | -SearchBenchmark2.benchmarkLatest thrpt 200 376546078,535 ? 1343521,015 ops/s |
201 | | -SearchBenchmark2.benchmarkRange thrpt 200 18960847,908 ? 60166,410 ops/s |
202 | | -SearchBenchmark2.benchmarkSnapshot thrpt 200 6079969,349 ? 71723,323 ops/s |
203 | | -SearchBenchmark2.benchmarkUpdates thrpt 200 36036591,098 ? 164357,240 ops/s |
204 | | -``` |
| 193 | +### ADR-7: No connection pool (plain JDBC with auto-reconnect) |
205 | 194 |
|
206 | | -100 attributes with 100K randomly distributed records |
| 195 | +`MariaDbSink` uses a single JDBC connection with `isValid()` check before each use and silent reconnect on failure. Rationale: domain events are low-frequency (state changes, not every poll); a full connection pool (HikariCP etc.) adds a dependency and warm-up complexity for negligible benefit at this throughput. |
207 | 196 |
|
208 | | -``` |
209 | | -# Run complete. Total time: 00:27:07 |
| 197 | +### ADR-8: Java 21 virtual threads |
210 | 198 |
|
211 | | -Benchmark Mode Cnt Score Error Units |
212 | | -SearchBenchmark3.benchmarkLatest thrpt 200 368993808,387 ? 1728088,058 ops/s |
213 | | -SearchBenchmark3.benchmarkRange thrpt 200 18638307,115 ? 67312,940 ops/s |
214 | | -SearchBenchmark3.benchmarkSnapshot thrpt 200 1785893,066 ? 23765,739 ops/s |
215 | | -SearchBenchmark3.benchmarkUpdates thrpt 200 36079956,416 ? 133265,984 ops/s |
216 | | -``` |
| 199 | +The HTTP server and engine polling tasks use `Thread.ofVirtual()`. This allows a large number of concurrent blocking I/O operations (Tango/TINE reads) without the overhead of a large platform thread pool. |
0 commit comments