microcosm-emitter-interface 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. microcosm_emitter_interface/__init__.py +28 -0
  2. microcosm_emitter_interface/client.py +355 -0
  3. microcosm_emitter_interface/fixtures/protocol-v1/host-close.json +10 -0
  4. microcosm_emitter_interface/fixtures/protocol-v1/host-hello.json +14 -0
  5. microcosm_emitter_interface/fixtures/protocol-v1/host-launch.json +22 -0
  6. microcosm_emitter_interface/fixtures/protocol-v1/host-message-envelope.json +13 -0
  7. microcosm_emitter_interface/fixtures/protocol-v1/host-ping.json +9 -0
  8. microcosm_emitter_interface/fixtures/protocol-v1/host-query-envelope.json +14 -0
  9. microcosm_emitter_interface/fixtures/protocol-v1/orrery-configuration.json +10 -0
  10. microcosm_emitter_interface/fixtures/protocol-v1/orrery-enqueue.json +35 -0
  11. microcosm_emitter_interface/fixtures/protocol-v1/orrery-inventory.json +22 -0
  12. microcosm_emitter_interface/fixtures/protocol-v1/orrery-receipt.json +13 -0
  13. microcosm_emitter_interface/fixtures/protocol-v1/orrery-retry.json +13 -0
  14. microcosm_emitter_interface/fixtures/protocol-v1/telemetry-calibration-progress.json +15 -0
  15. microcosm_emitter_interface/fixtures/protocol-v1/telemetry-complete.json +9 -0
  16. microcosm_emitter_interface/fixtures/protocol-v1/telemetry-configuration.json +22 -0
  17. microcosm_emitter_interface/fixtures/protocol-v1/telemetry-emit.json +16 -0
  18. microcosm_emitter_interface/fixtures/protocol-v1/telemetry-fail-without-code.json +13 -0
  19. microcosm_emitter_interface/fixtures/protocol-v1/telemetry-fail.json +14 -0
  20. microcosm_emitter_interface/fixtures/protocol-v1/telemetry-progress.json +16 -0
  21. microcosm_emitter_interface/fixtures/protocol-v1/telemetry-refusal.json +17 -0
  22. microcosm_emitter_interface/fixtures/protocol-v1/telemetry-stage.json +15 -0
  23. microcosm_emitter_interface/fixtures/protocol-v1/telemetry-transition-calibration-progress.json +15 -0
  24. microcosm_emitter_interface/fixtures/protocol-v1/telemetry-transition-stage.json +13 -0
  25. microcosm_emitter_interface/launcher.py +273 -0
  26. microcosm_emitter_interface/orrery.py +156 -0
  27. microcosm_emitter_interface/protocol.py +32 -0
  28. microcosm_emitter_interface/protocol_fixtures.py +140 -0
  29. microcosm_emitter_interface/py.typed +0 -0
  30. microcosm_emitter_interface/serialization.py +28 -0
  31. microcosm_emitter_interface/spool.py +11 -0
  32. microcosm_emitter_interface/telemetry.py +351 -0
  33. microcosm_emitter_interface/transport.py +139 -0
  34. microcosm_emitter_interface-0.5.0.dist-info/METADATA +45 -0
  35. microcosm_emitter_interface-0.5.0.dist-info/RECORD +37 -0
  36. microcosm_emitter_interface-0.5.0.dist-info/WHEEL +4 -0
  37. microcosm_emitter_interface-0.5.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,28 @@
1
+ """Typed hand-overs from Microcosm builds to the separately installed emitter.
2
+
3
+ Importing this package starts no process and opens no socket or file.
4
+ """
5
+
6
+ from microcosm_emitter_interface.client import EmitterClient, RequestFailure
7
+ from microcosm_emitter_interface.launcher import (
8
+ COMMAND_ENVIRONMENT_VARIABLE,
9
+ DEFAULT_COMMAND,
10
+ EmitterStartupError,
11
+ )
12
+ from microcosm_emitter_interface.orrery import OrrerySettings
13
+ from microcosm_emitter_interface.protocol import PROTOCOL_VERSION
14
+ from microcosm_emitter_interface.spool import default_spool_path
15
+ from microcosm_emitter_interface.telemetry import TelemetryRun, TelemetrySettings
16
+
17
+ __all__ = [
18
+ "COMMAND_ENVIRONMENT_VARIABLE",
19
+ "DEFAULT_COMMAND",
20
+ "PROTOCOL_VERSION",
21
+ "EmitterClient",
22
+ "EmitterStartupError",
23
+ "OrrerySettings",
24
+ "RequestFailure",
25
+ "TelemetryRun",
26
+ "TelemetrySettings",
27
+ "default_spool_path",
28
+ ]
@@ -0,0 +1,355 @@
1
+ """One typed call per kind of hand-over from a build to its local emitter.
2
+
3
+ Starting the client starts the installed ``microcosm-emitter`` command as this
4
+ process's child. No call raises into the build: not when the emitter is missing,
5
+ slow or refusing, and not when an argument cannot be converted to JSON (for
6
+ example a multi-element NumPy array or a self-referencing mapping). Each call
7
+ converts its arguments inside its error handling, reports that it was not
8
+ accepted, records why in ``EmitterClient.last_failure``, and the build continues.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import sys
15
+ import threading
16
+ import time
17
+ from collections.abc import Callable, Mapping
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Any, Literal
21
+
22
+ from microcosm_emitter_interface.launcher import (
23
+ EmitterProcess,
24
+ EmitterStartupError,
25
+ start_emitter,
26
+ )
27
+ from microcosm_emitter_interface.orrery import (
28
+ OrrerySettings,
29
+ PublicationFileRole,
30
+ PublicationInventory,
31
+ PublicationReceipt,
32
+ answered_inventory,
33
+ answered_receipt,
34
+ enqueue_message,
35
+ inventory_message,
36
+ publication_id_of,
37
+ receipt_message,
38
+ retry_message,
39
+ unavailable_receipt,
40
+ )
41
+ from microcosm_emitter_interface.protocol import (
42
+ ACKNOWLEDGEMENT_TIMEOUT_SECONDS,
43
+ FILE_REQUEST_TIMEOUT_SECONDS,
44
+ ORRERY_MODULE,
45
+ STARTUP_TIMEOUT_SECONDS,
46
+ TELEMETRY_MODULE,
47
+ )
48
+ from microcosm_emitter_interface.spool import default_spool_path
49
+ from microcosm_emitter_interface.telemetry import (
50
+ TelemetryCommand,
51
+ TelemetryRecorder,
52
+ TelemetrySettings,
53
+ telemetry_message,
54
+ )
55
+
56
+ STARTUP_WARNING = (
57
+ "Local Microcosm emitter failed to start ({reason}); the build will continue."
58
+ )
59
+ REQUEST_WARNING = (
60
+ "The local Microcosm emitter did not accept a {kind} request "
61
+ "({reason}, {error_type}); the build will continue."
62
+ )
63
+
64
+ type RequestFailureReason = Literal[
65
+ # No emitter is running, or it did not load the call's module.
66
+ "emitter_unavailable",
67
+ # The call's arguments could not be converted to a protocol message.
68
+ "invalid_arguments",
69
+ # The emitter refused the request, or did not answer in time.
70
+ "not_accepted",
71
+ ]
72
+
73
+ type MessageBuilder = Callable[[], Mapping[str, Any]]
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class RequestFailure:
78
+ """Why a call reported that the emitter did not accept it.
79
+
80
+ ``error_type`` is only the exception's type name, never its message.
81
+ """
82
+
83
+ kind: str
84
+ reason: RequestFailureReason
85
+ error_type: str | None = None
86
+
87
+
88
+ class EmitterClient:
89
+ """Typed hand-overs to one emitter process owned by this build."""
90
+
91
+ def __init__(
92
+ self,
93
+ process: EmitterProcess | None = None,
94
+ *,
95
+ startup_error: EmitterStartupError | None = None,
96
+ ) -> None:
97
+ self._process = process
98
+ self.startup_error = startup_error
99
+ self._closed = False
100
+ self._warned: set[str] = set()
101
+ self._warning_lock = threading.Lock()
102
+ #: The most recent call that reported failure, and why.
103
+ self.last_failure: RequestFailure | None = None
104
+ self.telemetry = TelemetryRecorder(self)
105
+
106
+ @classmethod
107
+ def start(
108
+ cls,
109
+ *,
110
+ telemetry: TelemetrySettings | None = None,
111
+ orrery: OrrerySettings | None = None,
112
+ spool_path: Path | str | None = None,
113
+ command: str | os.PathLike[str] | None = None,
114
+ startup_timeout: float = STARTUP_TIMEOUT_SECONDS,
115
+ ) -> EmitterClient:
116
+ """Start the selected modules; return an unavailable client on failure."""
117
+ try:
118
+ path = Path(spool_path) if spool_path is not None else default_spool_path()
119
+ modules: dict[str, dict[str, Any]] = {}
120
+ if telemetry is not None:
121
+ modules[TELEMETRY_MODULE] = telemetry.configuration(path)
122
+ if orrery is not None:
123
+ modules[ORRERY_MODULE] = orrery.configuration(path)
124
+ except Exception:
125
+ failure = EmitterStartupError("invalid_configuration")
126
+ print(STARTUP_WARNING.format(reason=failure.reason), file=sys.stderr)
127
+ return cls(startup_error=failure)
128
+ try:
129
+ process = start_emitter(
130
+ modules, command=command, startup_timeout=startup_timeout
131
+ )
132
+ except EmitterStartupError as error:
133
+ print(STARTUP_WARNING.format(reason=error.reason), file=sys.stderr)
134
+ return cls(startup_error=error)
135
+ except Exception:
136
+ failure = EmitterStartupError("startup_failure")
137
+ print(STARTUP_WARNING.format(reason=failure.reason), file=sys.stderr)
138
+ return cls(startup_error=failure)
139
+ return cls(process)
140
+
141
+ @property
142
+ def available(self) -> bool:
143
+ return self._process is not None and not self._closed
144
+
145
+ @property
146
+ def modules(self) -> frozenset[str]:
147
+ """The modules the running emitter loaded."""
148
+ if not self.available:
149
+ return frozenset()
150
+ assert self._process is not None
151
+ return frozenset(self._process.description.get("modules", ()))
152
+
153
+ @property
154
+ def emitter_version(self) -> str | None:
155
+ if self._process is None:
156
+ return None
157
+ version = self._process.description.get("emitter_version")
158
+ return version if isinstance(version, str) else None
159
+
160
+ def _fail(
161
+ self,
162
+ kind: str,
163
+ reason: RequestFailureReason,
164
+ error: BaseException | None = None,
165
+ ) -> None:
166
+ """Record why a call failed; warn once per kind of call.
167
+
168
+ A missing emitter was already reported when it failed to start, so
169
+ only refused requests and unconvertible arguments print a warning.
170
+ """
171
+ error_type = None if error is None else type(error).__name__
172
+ self.last_failure = RequestFailure(kind, reason, error_type)
173
+ if reason == "emitter_unavailable":
174
+ return
175
+ with self._warning_lock:
176
+ if kind in self._warned:
177
+ return
178
+ self._warned.add(kind)
179
+ print(
180
+ REQUEST_WARNING.format(kind=kind, reason=reason, error_type=error_type),
181
+ file=sys.stderr,
182
+ )
183
+
184
+ def _message(
185
+ self, build_message: MessageBuilder, *, kind: str
186
+ ) -> dict[str, Any] | None:
187
+ """The converted message, or None after recording why it failed."""
188
+ try:
189
+ return dict(build_message())
190
+ except Exception as error:
191
+ self._fail(kind, "invalid_arguments", error)
192
+ return None
193
+
194
+ def _send(self, module: str, build_message: MessageBuilder, *, kind: str) -> bool:
195
+ if module not in self.modules:
196
+ self._fail(kind, "emitter_unavailable")
197
+ return False
198
+ message = self._message(build_message, kind=kind)
199
+ if message is None:
200
+ return False
201
+ assert self._process is not None
202
+ try:
203
+ self._process.transport.send(
204
+ module, message, timeout=ACKNOWLEDGEMENT_TIMEOUT_SECONDS
205
+ )
206
+ except Exception as error:
207
+ self._fail(kind, "not_accepted", error)
208
+ return False
209
+ return True
210
+
211
+ def _request(
212
+ self,
213
+ module: str,
214
+ build_message: MessageBuilder,
215
+ *,
216
+ kind: str,
217
+ timeout: float = FILE_REQUEST_TIMEOUT_SECONDS,
218
+ ) -> tuple[dict[str, Any] | None, RequestFailureReason | None]:
219
+ """The module's answer, or None and why this call failed.
220
+
221
+ The reason belongs to this call only. ``last_failure`` is shared by
222
+ every call and thread, so a receipt's error code never reads it.
223
+ """
224
+ if module not in self.modules:
225
+ self._fail(kind, "emitter_unavailable")
226
+ return None, "emitter_unavailable"
227
+ message = self._message(build_message, kind=kind)
228
+ if message is None:
229
+ return None, "invalid_arguments"
230
+ assert self._process is not None
231
+ try:
232
+ return self._process.transport.query(module, message, timeout=timeout), None
233
+ except Exception as error:
234
+ self._fail(kind, "not_accepted", error)
235
+ return None, "not_accepted"
236
+
237
+ def _query(
238
+ self,
239
+ module: str,
240
+ build_message: MessageBuilder,
241
+ *,
242
+ kind: str,
243
+ timeout: float = FILE_REQUEST_TIMEOUT_SECONDS,
244
+ ) -> dict[str, Any] | None:
245
+ answer, _ = self._request(module, build_message, kind=kind, timeout=timeout)
246
+ return answer
247
+
248
+ def emit_telemetry(self, command: TelemetryCommand) -> bool:
249
+ """Hand over one telemetry event; True once the emitter committed it."""
250
+ return self._send(
251
+ TELEMETRY_MODULE, lambda: telemetry_message(command), kind="telemetry"
252
+ )
253
+
254
+ def orrery_inventory(
255
+ self,
256
+ directory: Path,
257
+ roles: Mapping[str, PublicationFileRole],
258
+ *,
259
+ publication_id: str | None = None,
260
+ ) -> PublicationInventory | None:
261
+ """Ask the emitter to measure the selected preserved Orrery input files."""
262
+ response = self._query(
263
+ ORRERY_MODULE,
264
+ lambda: inventory_message(directory, roles, publication_id),
265
+ kind="Orrery",
266
+ )
267
+ return answered_inventory(response)
268
+
269
+ def hand_over_orrery_inputs(
270
+ self,
271
+ directory: Path,
272
+ inventory: PublicationInventory,
273
+ *,
274
+ wait_seconds: float = 30.0,
275
+ ) -> PublicationReceipt:
276
+ """Queue the inventoried files; wait only after the durable enqueue."""
277
+ publication_id = publication_id_of(inventory)
278
+ if publication_id is None:
279
+ self._fail("Orrery", "invalid_arguments")
280
+ return unavailable_receipt(None, error_code="invalid_arguments")
281
+ receipt = self._orrery_receipt_request(
282
+ lambda: enqueue_message(directory, inventory), publication_id
283
+ )
284
+ deadline = time.monotonic() + _wait_seconds(wait_seconds)
285
+ while (
286
+ self.available
287
+ and receipt["status"] == "pending"
288
+ and not receipt.get("attempts", 0)
289
+ and time.monotonic() < deadline
290
+ ):
291
+ time.sleep(0.1)
292
+ receipt = self.orrery_receipt(publication_id)
293
+ return receipt
294
+
295
+ def orrery_receipt(self, publication_id: str) -> PublicationReceipt:
296
+ return self._orrery_receipt_request(
297
+ lambda: receipt_message(publication_id), publication_id
298
+ )
299
+
300
+ def retry_orrery_inputs(
301
+ self, publication_id: str, *, wait_seconds: float = 0
302
+ ) -> PublicationReceipt:
303
+ receipt = self._orrery_receipt_request(
304
+ lambda: retry_message(publication_id), publication_id
305
+ )
306
+ previous_attempts = receipt.get("attempts", 0)
307
+ deadline = time.monotonic() + _wait_seconds(wait_seconds)
308
+ while (
309
+ self.available
310
+ and receipt["status"] == "pending"
311
+ and receipt.get("attempts", 0) == previous_attempts
312
+ and time.monotonic() < deadline
313
+ ):
314
+ time.sleep(0.1)
315
+ receipt = self.orrery_receipt(publication_id)
316
+ return receipt
317
+
318
+ def _orrery_receipt_request(
319
+ self, build_message: MessageBuilder, publication_id: str | None
320
+ ) -> PublicationReceipt:
321
+ answer, failure = self._request(ORRERY_MODULE, build_message, kind="Orrery")
322
+ return answered_receipt(
323
+ answer, publication_id, error_code=_receipt_error_code(failure)
324
+ )
325
+
326
+ def close(self) -> None:
327
+ """Ask the emitter to drain and stop; repeated calls do nothing."""
328
+ if self._closed:
329
+ return
330
+ self._closed = True
331
+ if self._process is not None:
332
+ try:
333
+ self._process.close()
334
+ except Exception as error:
335
+ self._fail("close", "not_accepted", error)
336
+
337
+
338
+ def _receipt_error_code(failure: RequestFailureReason | None) -> str:
339
+ """The error code of an unavailable receipt, from its own call's failure.
340
+
341
+ An answer without a receipt, or no answer, is ``local_service_unavailable``,
342
+ as in the emitter's in-process client.
343
+ """
344
+ if failure == "invalid_arguments":
345
+ return "invalid_arguments"
346
+ return "local_service_unavailable"
347
+
348
+
349
+ def _wait_seconds(value: Any) -> float:
350
+ """A usable waiting time; an unusable one means not waiting."""
351
+ try:
352
+ seconds = float(value)
353
+ except Exception:
354
+ return 0.0
355
+ return seconds if seconds > 0 else 0.0
@@ -0,0 +1,10 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "host",
4
+ "kind": "envelope",
5
+ "sequence": 1,
6
+ "body": {
7
+ "action": "close"
8
+ },
9
+ "response": "acknowledgement"
10
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "host",
4
+ "kind": "hello",
5
+ "body": {
6
+ "action": "hello"
7
+ },
8
+ "response_keys": [
9
+ "emitter_version",
10
+ "modules",
11
+ "protocol_versions",
12
+ "skipped_modules"
13
+ ]
14
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "host",
4
+ "kind": "launch",
5
+ "body": {
6
+ "arguments": [
7
+ "host",
8
+ "--socket",
9
+ "{socket_path}",
10
+ "--parent-pid",
11
+ "{parent_pid}",
12
+ "--skip-unavailable-modules"
13
+ ],
14
+ "standard_input": {
15
+ "orrery": {
16
+ "spool_path": "{spool_path}",
17
+ "development_collector_url": "http://127.0.0.1:1",
18
+ "publication_origin": "http://127.0.0.1:1"
19
+ }
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "host",
4
+ "kind": "envelope",
5
+ "body": {
6
+ "action": "message",
7
+ "module": "telemetry",
8
+ "message": {
9
+ "command": "complete"
10
+ }
11
+ },
12
+ "response": "acknowledgement"
13
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "host",
4
+ "kind": "envelope",
5
+ "body": {
6
+ "action": "ping"
7
+ },
8
+ "response": "acknowledgement"
9
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "host",
4
+ "kind": "envelope",
5
+ "body": {
6
+ "action": "query",
7
+ "module": "orrery",
8
+ "message": {
9
+ "command": "receipt",
10
+ "publication_id": "fixture-publication"
11
+ }
12
+ },
13
+ "response": "answer"
14
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "orrery",
4
+ "kind": "configuration",
5
+ "body": {
6
+ "spool_path": "{spool_path}",
7
+ "development_collector_url": "http://127.0.0.1:1",
8
+ "publication_origin": "http://127.0.0.1:1"
9
+ }
10
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "orrery",
4
+ "kind": "query",
5
+ "sequence": 2,
6
+ "files": {
7
+ "graph.orrery.json": "{\"nodes\":[]}",
8
+ "graph.schema.json": "{\"schema\":true}"
9
+ },
10
+ "body": {
11
+ "command": "enqueue",
12
+ "directory": "{directory}",
13
+ "inventory": {
14
+ "version": 1,
15
+ "publication_id": "fixture-publication",
16
+ "files": [
17
+ {
18
+ "name": "graph.orrery.json",
19
+ "role": "graph",
20
+ "bytes": 12,
21
+ "sha256": "acf2fa576acb702442f9d0101673354c398db67315c066ca48be8db8e0d2c75b"
22
+ },
23
+ {
24
+ "name": "graph.schema.json",
25
+ "role": "schema",
26
+ "bytes": 15,
27
+ "sha256": "e296bc1725afc9c1e1585549e7f3c3b0cf58acba82fc60188d8eda40b9e17a13"
28
+ }
29
+ ]
30
+ }
31
+ },
32
+ "response_keys": [
33
+ "receipt"
34
+ ]
35
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "orrery",
4
+ "kind": "query",
5
+ "sequence": 1,
6
+ "files": {
7
+ "graph.orrery.json": "{\"nodes\":[]}",
8
+ "graph.schema.json": "{\"schema\":true}"
9
+ },
10
+ "body": {
11
+ "command": "inventory",
12
+ "directory": "{directory}",
13
+ "roles": {
14
+ "graph.orrery.json": "graph",
15
+ "graph.schema.json": "schema"
16
+ },
17
+ "publication_id": "fixture-publication"
18
+ },
19
+ "response_keys": [
20
+ "inventory"
21
+ ]
22
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "orrery",
4
+ "kind": "query",
5
+ "sequence": 3,
6
+ "body": {
7
+ "command": "receipt",
8
+ "publication_id": "fixture-publication"
9
+ },
10
+ "response_keys": [
11
+ "receipt"
12
+ ]
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "orrery",
4
+ "kind": "query",
5
+ "sequence": 4,
6
+ "body": {
7
+ "command": "retry",
8
+ "publication_id": "fixture-publication"
9
+ },
10
+ "response_keys": [
11
+ "receipt"
12
+ ]
13
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "telemetry",
4
+ "kind": "message",
5
+ "sequence": 0,
6
+ "body": {
7
+ "command": "calibration_progress",
8
+ "event": {
9
+ "kind": "calibration_epoch",
10
+ "epoch": 10,
11
+ "epochs": 100,
12
+ "loss": 0.25
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "telemetry",
4
+ "kind": "message",
5
+ "sequence": 0,
6
+ "body": {
7
+ "command": "complete"
8
+ }
9
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "telemetry",
4
+ "kind": "configuration",
5
+ "body": {
6
+ "registration": {
7
+ "run_id": "fixture-run",
8
+ "producer_id": "fixture-producer",
9
+ "country_code": "UK",
10
+ "pipeline": "fixture-pipeline",
11
+ "candidate_id": null,
12
+ "release_id": null,
13
+ "run_kind": "build"
14
+ },
15
+ "spool_path": "{spool_path}",
16
+ "identity": {
17
+ "host": "fixture"
18
+ },
19
+ "development_collector_url": "http://127.0.0.1:1",
20
+ "heartbeat_seconds": 60.0
21
+ }
22
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "telemetry",
4
+ "kind": "message",
5
+ "sequence": 0,
6
+ "body": {
7
+ "command": "emit",
8
+ "event_type": "progress",
9
+ "stage_id": "compile",
10
+ "status": "progress",
11
+ "message": "Compiling.",
12
+ "details": {
13
+ "step": 1
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "telemetry",
4
+ "kind": "message",
5
+ "sequence": 0,
6
+ "body": {
7
+ "command": "fail",
8
+ "message": "Synthetic failure.",
9
+ "error_type": "MemoryError",
10
+ "failed_during": null,
11
+ "failure_class": "oom"
12
+ }
13
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "protocol_version": 1,
3
+ "module": "telemetry",
4
+ "kind": "message",
5
+ "sequence": 0,
6
+ "body": {
7
+ "command": "fail",
8
+ "message": "Synthetic failure.",
9
+ "error_type": "RuntimeError",
10
+ "failed_during": "calibrating",
11
+ "failure_class": "build_failure",
12
+ "error_code": "BUILD_FAILED"
13
+ }
14
+ }