taskflow-meter 1.0.0__tar.gz → 1.2.0__tar.gz
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.
- taskflow_meter-1.0.0/README.md → taskflow_meter-1.2.0/PKG-INFO +95 -6
- taskflow_meter-1.0.0/PKG-INFO → taskflow_meter-1.2.0/README.md +54 -56
- taskflow_meter-1.2.0/docs/design.md +171 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/docs/guide.md +42 -4
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/pyproject.toml +49 -35
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/__init__.py +2 -1
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/_version.py +2 -2
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/sse.py +4 -1
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/cli.py +63 -7
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/listener.py +20 -1
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/conf.py +6 -1
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/base.py +4 -2
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/persistence.py +5 -13
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/migrations/env.py +17 -1
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/source.py +15 -1
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/events.py +21 -3
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/fold.py +6 -1
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/meter.py +4 -2
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/models.py +20 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/amqp.py +4 -2
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/base.py +22 -3
- taskflow_meter-1.2.0/taskflow_meter/transports/oslo_messaging.py +355 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/conformance/test_parity.py +4 -4
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/functional/test_cross_process.py +1 -1
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/functional/test_packaging.py +63 -6
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/integration/test_attach.py +48 -1
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/integration/test_collector.py +59 -1
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/datasource/sqlalchemy/test_source.py +20 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_cli.py +94 -0
- taskflow_meter-1.2.0/tests/unit/transports/test_oslo_messaging.py +425 -0
- taskflow_meter-1.0.0/docs/PLAN.md +0 -439
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/.gitignore +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/LICENSE +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/docs/releasing.md +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/asgi.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/dispatch.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/http.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/router.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/routes.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/serializers.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/service.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/api/wsgi.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/attachment.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/pipeline.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/collect/progress.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/django.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/fastapi.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/flask.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/paste.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/contrib/pecan.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/memory.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/migrations/script.py.mako +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/migrations/versions/0001_initial.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/datasource/sqlalchemy/models.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/diff.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/poller.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/py.typed +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/states.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/http.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/taskflow_meter/transports/memory.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/asgi_client.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/conformance/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/conformance/test_hosts.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/conftest.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/functional/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/functional/test_examples.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/functional/test_migrations.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/functional/test_serve.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/functional/test_tooling.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/hosts.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/integration/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_asgi.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_http.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_router.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_routes.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_serializers.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_service.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_sse.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/api/test_wsgi.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/collect/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/collect/test_attachment.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/collect/test_listener.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/collect/test_pipeline.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/collect/test_progress.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/contrib/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_django.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_fastapi.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_flask.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_paste.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/contrib/test_pecan.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/datasource/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/datasource/sqlalchemy/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/datasource/test_base.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/datasource/test_memory.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/datasource/test_persistence.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_conf.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_diff.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_events.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_meter.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_models.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_poller.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/test_states.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/transports/__init__.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/transports/test_amqp.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/transports/test_http.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/unit/transports/test_memory.py +0 -0
- {taskflow_meter-1.0.0 → taskflow_meter-1.2.0}/tests/wsgi_client.py +0 -0
|
@@ -1,12 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: taskflow-meter
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Monitoring interfaces (ASGI, WSGI, datasource) for OpenStack TaskFlow flow execution progress
|
|
5
|
+
Project-URL: Homepage, https://github.com/daipham3213/taskflow.meter
|
|
6
|
+
Project-URL: Source, https://github.com/daipham3213/taskflow.meter
|
|
7
|
+
Project-URL: Issues, https://github.com/daipham3213/taskflow.meter/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/daipham3213/taskflow.meter/releases
|
|
9
|
+
Author-email: Pham Le Gia Dai <daipham.3213@gmail.com>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: asgi,monitoring,openstack,taskflow,workflow,wsgi
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: System :: Monitoring
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: oslo-config>=6.9.0
|
|
27
|
+
Requires-Dist: taskflow>=4.2.0
|
|
28
|
+
Provides-Extra: all
|
|
29
|
+
Requires-Dist: alembic>=1.2.0; extra == 'all'
|
|
30
|
+
Requires-Dist: kombu>=5.1.0; extra == 'all'
|
|
31
|
+
Requires-Dist: oslo-messaging>=6.0.0; extra == 'all'
|
|
32
|
+
Requires-Dist: sqlalchemy>=1.4.0; extra == 'all'
|
|
33
|
+
Provides-Extra: amqp
|
|
34
|
+
Requires-Dist: kombu>=5.1.0; extra == 'amqp'
|
|
35
|
+
Provides-Extra: oslo-messaging
|
|
36
|
+
Requires-Dist: oslo-messaging>=6.0.0; extra == 'oslo-messaging'
|
|
37
|
+
Provides-Extra: sqlalchemy
|
|
38
|
+
Requires-Dist: alembic>=1.2.0; extra == 'sqlalchemy'
|
|
39
|
+
Requires-Dist: sqlalchemy>=1.4.0; extra == 'sqlalchemy'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
1
42
|
# taskflow-meter
|
|
2
43
|
|
|
44
|
+
[](https://github.com/daipham3213/taskflow.meter/actions/workflows/ci.yml)
|
|
45
|
+
[](https://github.com/daipham3213/taskflow.meter/actions/workflows/conformance.yml)
|
|
46
|
+
[](https://pypi.org/project/taskflow-meter/)
|
|
47
|
+
[](https://pypi.org/project/taskflow-meter/)
|
|
48
|
+
[](LICENSE)
|
|
49
|
+
|
|
3
50
|
Monitoring interfaces — ASGI, WSGI, datasources, transports — for observing
|
|
4
51
|
[OpenStack TaskFlow](https://opendev.org/openstack/taskflow) flow execution
|
|
5
52
|
progress.
|
|
6
53
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
54
|
+
```bash
|
|
55
|
+
pip install taskflow-meter
|
|
56
|
+
```
|
|
10
57
|
|
|
11
58
|
## What it is for
|
|
12
59
|
|
|
@@ -73,9 +120,13 @@ polling anything.
|
|
|
73
120
|
|
|
74
121
|
```bash
|
|
75
122
|
taskflow-meter upgrade --store-url postgresql://host/meter
|
|
76
|
-
taskflow-meter collect --
|
|
123
|
+
taskflow-meter collect --url amqp://broker// --store-url postgresql://host/meter
|
|
77
124
|
taskflow-meter serve --store-url postgresql://host/meter
|
|
78
125
|
```
|
|
126
|
+
|
|
127
|
+
Inside OpenStack, `--transport oslo-messaging` puts the events on the
|
|
128
|
+
notification bus the service is already configured for, instead of opening a
|
|
129
|
+
broker connection of its own.
|
|
79
130
|
One caveat for WSGI deployments: a synchronous worker holds a thread for as
|
|
80
131
|
long as an SSE stream stays open, so use gevent or eventlet workers for
|
|
81
132
|
streaming, or let clients poll `/events?since_seq=` instead.
|
|
@@ -156,8 +207,37 @@ serving an empty stream that cannot be told apart from silence.
|
|
|
156
207
|
|
|
157
208
|
## Requirements
|
|
158
209
|
|
|
159
|
-
- Python 3.
|
|
160
|
-
- taskflow
|
|
210
|
+
- Python 3.10+
|
|
211
|
+
- taskflow 4.2.0+
|
|
212
|
+
- oslo.config 6.9.0+
|
|
213
|
+
|
|
214
|
+
Floors are deliberately low: this package is meant to be co-installed into
|
|
215
|
+
a service whose dependency versions it does not get to choose. They are the
|
|
216
|
+
oldest release of each library the suite actually passes against, not the
|
|
217
|
+
oldest that looks plausible -- a `lowest-direct` CI job installs exactly
|
|
218
|
+
these and runs the whole suite on them.
|
|
219
|
+
|
|
220
|
+
Everything else is optional, and only needed by the feature that imports it:
|
|
221
|
+
|
|
222
|
+
| Extra | Pulls in | Needed for |
|
|
223
|
+
| --- | --- | --- |
|
|
224
|
+
| `sqlalchemy` | SQLAlchemy 1.4+, alembic 1.2+ | The collector's own store |
|
|
225
|
+
| `amqp` | kombu 5.1+ | Publishing events to a broker |
|
|
226
|
+
| `oslo-messaging` | oslo.messaging 6.0+ | Publishing onto the service's own notification bus |
|
|
227
|
+
| `all` | all three | |
|
|
228
|
+
|
|
229
|
+
The contrib adapters declare no dependency on their hosts -- a deployment
|
|
230
|
+
mounting the meter in Django already has Django. They are tested against
|
|
231
|
+
Django 3.2, Flask 2.3.3, FastAPI 0.100, Pecan 1.4 and PasteDeploy 2.0.
|
|
232
|
+
|
|
233
|
+
## Documentation
|
|
234
|
+
|
|
235
|
+
| | |
|
|
236
|
+
| --- | --- |
|
|
237
|
+
| [`docs/guide.md`](docs/guide.md) | Deploying it: configuration, every host, and how to read completion and current-task out of the API |
|
|
238
|
+
| [`docs/design.md`](docs/design.md) | How it works and why -- what taskflow does and does not record, and the rules the embedding code obeys |
|
|
239
|
+
| [`docs/releasing.md`](docs/releasing.md) | Cutting a release |
|
|
240
|
+
| [`CHANGELOG.md`](CHANGELOG.md) | What changed |
|
|
161
241
|
|
|
162
242
|
## Examples
|
|
163
243
|
|
|
@@ -186,6 +266,15 @@ uvx tox -e py312 # tests on one interpreter
|
|
|
186
266
|
uvx tox # the whole matrix
|
|
187
267
|
```
|
|
188
268
|
|
|
269
|
+
CI also runs the suite with every declared dependency floor installed exactly,
|
|
270
|
+
which is the only job that checks those floors are real. To reproduce it:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
uv lock --python 3.10 --resolution lowest-direct
|
|
274
|
+
uv sync --python 3.10 --group dev --all-extras --resolution lowest-direct
|
|
275
|
+
uv run --frozen --no-sync pytest
|
|
276
|
+
```
|
|
277
|
+
|
|
189
278
|
Because the version is derived from git history, a shallow clone or a checkout
|
|
190
279
|
with no tags will build as `0.0.0`. CI checks out with full history.
|
|
191
280
|
|
|
@@ -1,62 +1,18 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: taskflow-meter
|
|
3
|
-
Version: 1.0.0
|
|
4
|
-
Summary: Monitoring interfaces (ASGI, WSGI, datasource) for OpenStack TaskFlow flow execution progress
|
|
5
|
-
Project-URL: Homepage, https://github.com/daipham3213/taskflow-meter
|
|
6
|
-
Project-URL: Source, https://github.com/daipham3213/taskflow-meter
|
|
7
|
-
Project-URL: Issues, https://github.com/daipham3213/taskflow-meter/issues
|
|
8
|
-
Project-URL: Changelog, https://github.com/daipham3213/taskflow-meter/releases
|
|
9
|
-
Author-email: Pham Le Gia Dai <daipham.3213@gmail.com>
|
|
10
|
-
License-Expression: Apache-2.0
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Keywords: asgi,monitoring,openstack,taskflow,workflow,wsgi
|
|
13
|
-
Classifier: Development Status :: 4 - Beta
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: Intended Audience :: System Administrators
|
|
16
|
-
Classifier: Operating System :: OS Independent
|
|
17
|
-
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
-
Classifier: Topic :: System :: Monitoring
|
|
23
|
-
Classifier: Typing :: Typed
|
|
24
|
-
Requires-Python: >=3.11
|
|
25
|
-
Requires-Dist: oslo-cache>=3.0.0
|
|
26
|
-
Requires-Dist: oslo-config>=9.0.0
|
|
27
|
-
Requires-Dist: oslo-serialization>=2.18.0
|
|
28
|
-
Requires-Dist: oslo-utils>=3.33.0
|
|
29
|
-
Requires-Dist: stevedore>=1.20.0
|
|
30
|
-
Requires-Dist: taskflow>=6.4.0
|
|
31
|
-
Provides-Extra: all
|
|
32
|
-
Requires-Dist: alembic>=1.13; extra == 'all'
|
|
33
|
-
Requires-Dist: kombu>=5.0; extra == 'all'
|
|
34
|
-
Requires-Dist: oslo-cache[dogpile]>=3.0.0; extra == 'all'
|
|
35
|
-
Requires-Dist: oslo-cache[etcd3gw]>=3.0.0; extra == 'all'
|
|
36
|
-
Requires-Dist: prometheus-client>=0.20; extra == 'all'
|
|
37
|
-
Requires-Dist: sqlalchemy>=2.0; extra == 'all'
|
|
38
|
-
Provides-Extra: amqp
|
|
39
|
-
Requires-Dist: kombu>=5.0; extra == 'amqp'
|
|
40
|
-
Provides-Extra: etcd
|
|
41
|
-
Requires-Dist: oslo-cache[etcd3gw]>=3.0.0; extra == 'etcd'
|
|
42
|
-
Provides-Extra: memcache
|
|
43
|
-
Requires-Dist: oslo-cache[dogpile]>=3.0.0; extra == 'memcache'
|
|
44
|
-
Provides-Extra: prometheus
|
|
45
|
-
Requires-Dist: prometheus-client>=0.20; extra == 'prometheus'
|
|
46
|
-
Provides-Extra: sqlalchemy
|
|
47
|
-
Requires-Dist: alembic>=1.13; extra == 'sqlalchemy'
|
|
48
|
-
Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
|
|
49
|
-
Description-Content-Type: text/markdown
|
|
50
|
-
|
|
51
1
|
# taskflow-meter
|
|
52
2
|
|
|
3
|
+
[](https://github.com/daipham3213/taskflow.meter/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/daipham3213/taskflow.meter/actions/workflows/conformance.yml)
|
|
5
|
+
[](https://pypi.org/project/taskflow-meter/)
|
|
6
|
+
[](https://pypi.org/project/taskflow-meter/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
53
9
|
Monitoring interfaces — ASGI, WSGI, datasources, transports — for observing
|
|
54
10
|
[OpenStack TaskFlow](https://opendev.org/openstack/taskflow) flow execution
|
|
55
11
|
progress.
|
|
56
12
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
13
|
+
```bash
|
|
14
|
+
pip install taskflow-meter
|
|
15
|
+
```
|
|
60
16
|
|
|
61
17
|
## What it is for
|
|
62
18
|
|
|
@@ -123,9 +79,13 @@ polling anything.
|
|
|
123
79
|
|
|
124
80
|
```bash
|
|
125
81
|
taskflow-meter upgrade --store-url postgresql://host/meter
|
|
126
|
-
taskflow-meter collect --
|
|
82
|
+
taskflow-meter collect --url amqp://broker// --store-url postgresql://host/meter
|
|
127
83
|
taskflow-meter serve --store-url postgresql://host/meter
|
|
128
84
|
```
|
|
85
|
+
|
|
86
|
+
Inside OpenStack, `--transport oslo-messaging` puts the events on the
|
|
87
|
+
notification bus the service is already configured for, instead of opening a
|
|
88
|
+
broker connection of its own.
|
|
129
89
|
One caveat for WSGI deployments: a synchronous worker holds a thread for as
|
|
130
90
|
long as an SSE stream stays open, so use gevent or eventlet workers for
|
|
131
91
|
streaming, or let clients poll `/events?since_seq=` instead.
|
|
@@ -206,8 +166,37 @@ serving an empty stream that cannot be told apart from silence.
|
|
|
206
166
|
|
|
207
167
|
## Requirements
|
|
208
168
|
|
|
209
|
-
- Python 3.
|
|
210
|
-
- taskflow
|
|
169
|
+
- Python 3.10+
|
|
170
|
+
- taskflow 4.2.0+
|
|
171
|
+
- oslo.config 6.9.0+
|
|
172
|
+
|
|
173
|
+
Floors are deliberately low: this package is meant to be co-installed into
|
|
174
|
+
a service whose dependency versions it does not get to choose. They are the
|
|
175
|
+
oldest release of each library the suite actually passes against, not the
|
|
176
|
+
oldest that looks plausible -- a `lowest-direct` CI job installs exactly
|
|
177
|
+
these and runs the whole suite on them.
|
|
178
|
+
|
|
179
|
+
Everything else is optional, and only needed by the feature that imports it:
|
|
180
|
+
|
|
181
|
+
| Extra | Pulls in | Needed for |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| `sqlalchemy` | SQLAlchemy 1.4+, alembic 1.2+ | The collector's own store |
|
|
184
|
+
| `amqp` | kombu 5.1+ | Publishing events to a broker |
|
|
185
|
+
| `oslo-messaging` | oslo.messaging 6.0+ | Publishing onto the service's own notification bus |
|
|
186
|
+
| `all` | all three | |
|
|
187
|
+
|
|
188
|
+
The contrib adapters declare no dependency on their hosts -- a deployment
|
|
189
|
+
mounting the meter in Django already has Django. They are tested against
|
|
190
|
+
Django 3.2, Flask 2.3.3, FastAPI 0.100, Pecan 1.4 and PasteDeploy 2.0.
|
|
191
|
+
|
|
192
|
+
## Documentation
|
|
193
|
+
|
|
194
|
+
| | |
|
|
195
|
+
| --- | --- |
|
|
196
|
+
| [`docs/guide.md`](docs/guide.md) | Deploying it: configuration, every host, and how to read completion and current-task out of the API |
|
|
197
|
+
| [`docs/design.md`](docs/design.md) | How it works and why -- what taskflow does and does not record, and the rules the embedding code obeys |
|
|
198
|
+
| [`docs/releasing.md`](docs/releasing.md) | Cutting a release |
|
|
199
|
+
| [`CHANGELOG.md`](CHANGELOG.md) | What changed |
|
|
211
200
|
|
|
212
201
|
## Examples
|
|
213
202
|
|
|
@@ -236,6 +225,15 @@ uvx tox -e py312 # tests on one interpreter
|
|
|
236
225
|
uvx tox # the whole matrix
|
|
237
226
|
```
|
|
238
227
|
|
|
228
|
+
CI also runs the suite with every declared dependency floor installed exactly,
|
|
229
|
+
which is the only job that checks those floors are real. To reproduce it:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
uv lock --python 3.10 --resolution lowest-direct
|
|
233
|
+
uv sync --python 3.10 --group dev --all-extras --resolution lowest-direct
|
|
234
|
+
uv run --frozen --no-sync pytest
|
|
235
|
+
```
|
|
236
|
+
|
|
239
237
|
Because the version is derived from git history, a shallow clone or a checkout
|
|
240
238
|
with no tags will build as `0.0.0`. CI checks out with full history.
|
|
241
239
|
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# How taskflow-meter works, and why
|
|
2
|
+
|
|
3
|
+
The [guide](guide.md) covers deploying it. This covers the reasoning
|
|
4
|
+
underneath: what taskflow does and does not record, what follows from that,
|
|
5
|
+
and the rules the embedding code has to obey. It is here so the next person
|
|
6
|
+
to change something knows which parts are load-bearing.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. What taskflow gives us
|
|
11
|
+
|
|
12
|
+
Everything else follows from these. They are properties of taskflow, verified
|
|
13
|
+
against it rather than assumed. The suite keeps them honest from two
|
|
14
|
+
directions: `tests/functional/test_packaging.py` pins the API names the
|
|
15
|
+
package relies on, and `tests/functional/test_cross_process.py` proves the
|
|
16
|
+
behaviour that matters most -- progress reported by a task in one process
|
|
17
|
+
being readable from another.
|
|
18
|
+
|
|
19
|
+
| Fact | Consequence |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `engine.notifier` fires on flow state transitions, `engine.atom_notifier` on atom state transitions | A `Listener` sees state changes, and only state changes |
|
|
22
|
+
| `task.update_progress()` is handled by `TaskAction._on_update_progress`, which writes to storage and **never re-emits on `atom_notifier`** | A listener cannot see intra-task progress. Reading it live needs a separate tap on each atom's own notifier |
|
|
23
|
+
| `Task.TASK_EVENTS` is exactly `('update_progress',)` | That tap has one event to bind, and no others will appear |
|
|
24
|
+
| `storage.set_task_progress()` write-throughs to the persistence backend, as `meta['progress']` and `meta['progress_details']` | Fine-grained progress **is** durable, and readable from another process |
|
|
25
|
+
| `LogBook` has `created_at`/`updated_at`; `FlowDetail` and `AtomDetail` have neither | No per-atom start or end times exist to report. Observation times are stamped by the meter, and mean only when it looked |
|
|
26
|
+
| Flow DAG edges are not persisted -- a `FlowDetail` holds a flat set of atoms | Nothing read from persistence can draw the graph. Topology needs the in-process path |
|
|
27
|
+
| `Connection` exposes `get_logbooks()`, `get_flows_for_book()`, `get_atoms_for_flow()`, with no filtering or paging | Listing is a full scan |
|
|
28
|
+
| Progress callbacks are proxied back from remote workers | Worker-based engines report progress the same way |
|
|
29
|
+
| taskflow discovers backends through stevedore entry points | Our own plugins follow the same convention |
|
|
30
|
+
|
|
31
|
+
**The bottom line, and the reason this package can exist:** a deployment
|
|
32
|
+
already using a shared persistence backend can be monitored *including
|
|
33
|
+
per-task progress* with no changes to flow code. The in-process path is an
|
|
34
|
+
enhancement -- lower latency, plus the graph -- not a prerequisite.
|
|
35
|
+
|
|
36
|
+
The `memory` backend is the exception: per-process, and invisible from
|
|
37
|
+
anywhere else.
|
|
38
|
+
|
|
39
|
+
## 2. Decisions, and what they cost
|
|
40
|
+
|
|
41
|
+
| Decision | Why | What it costs |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| Read-only over taskflow persistence as the primary source | Monitors existing deployments untouched | Resolution is the poll interval; anything entered and left between two polls was never observable |
|
|
44
|
+
| Hand-rolled ASGI and WSGI callables, no web framework | Maximum embeddability: a service does not inherit Starlette because it wanted monitoring | Two implementations to keep identical, which is what the conformance suite is for |
|
|
45
|
+
| Both callables ship, rather than one plus a bridge | No `a2wsgi`-style shim in either direction | -- |
|
|
46
|
+
| Mountable at any sub-path, plus native adapters in `contrib/` | Standalone `serve` is one deployment mode, not the only one | Every link has to be built from a discovered prefix |
|
|
47
|
+
| oslo.config for configuration | What OpenStack operators expect; the service configures the meter in the file it already reads | -- |
|
|
48
|
+
| Completion derived from state, never from raw `progress` | taskflow sets progress to 1.0 on `REVERTED` as well as `SUCCESS`, so a mean of `progress` reports a fully reverted flow as complete | Two numbers in the payload instead of one |
|
|
49
|
+
|
|
50
|
+
## 3. The emit side must never hurt the flow
|
|
51
|
+
|
|
52
|
+
The rule the whole in-process path rests on: **nothing monitoring does can
|
|
53
|
+
change a flow's outcome or its timing measurably.** Concretely:
|
|
54
|
+
|
|
55
|
+
- A publisher that raises is counted and logged, and the next batch is still
|
|
56
|
+
attempted. It cannot propagate into the engine.
|
|
57
|
+
- Delivery happens on its own thread behind a **bounded** queue, so a task
|
|
58
|
+
never waits on a publisher. Blocking would stall the flow; growing without
|
|
59
|
+
limit would take the process down later, for a reason nobody would connect
|
|
60
|
+
back to monitoring.
|
|
61
|
+
- A full queue drops the oldest events, counts the drops, and logs. Losing
|
|
62
|
+
old monitoring data beats losing the process.
|
|
63
|
+
- A listener whose callback raises logs and carries on, and the flow finishes
|
|
64
|
+
normally.
|
|
65
|
+
|
|
66
|
+
The receiving side makes the opposite trade, deliberately: a handler that
|
|
67
|
+
fails leaves the batch on the broker rather than acknowledging it. Silently
|
|
68
|
+
dropping monitoring data is worse than redelivering it.
|
|
69
|
+
|
|
70
|
+
## 4. Embedding into somebody else's application
|
|
71
|
+
|
|
72
|
+
Three ways in, in increasing order of how much the host is involved:
|
|
73
|
+
|
|
74
|
+
1. **Mount the raw callable.** Works in anything that can mount an ASGI or
|
|
75
|
+
WSGI app. Nothing from this package is needed.
|
|
76
|
+
2. **A native adapter** from `contrib/`. The routes are registered in the
|
|
77
|
+
host's own router, so its authentication, middleware, error handling --
|
|
78
|
+
and, for FastAPI, its OpenAPI schema -- apply to them.
|
|
79
|
+
3. **Hand-wire it.** `api.routes.build_routes()` returns the route table as
|
|
80
|
+
data; register the handlers yourself.
|
|
81
|
+
|
|
82
|
+
### Mount-safety
|
|
83
|
+
|
|
84
|
+
- **Never trust `scope["path"]` alone.** Modern Starlette's `Mount` does not
|
|
85
|
+
rewrite `path`; it extends `root_path`. Older versions stripped `path`
|
|
86
|
+
instead. A server given `--root-path` never puts the prefix in `path` at
|
|
87
|
+
all. One rule covers all three: strip `root_path` from `path` **only when
|
|
88
|
+
it is genuinely a prefix**, otherwise use `path` unchanged. That is what
|
|
89
|
+
`api.http.split_path` does.
|
|
90
|
+
- **WSGI is unambiguous.** The server or dispatcher has already split
|
|
91
|
+
`SCRIPT_NAME` from `PATH_INFO`. Route on `PATH_INFO`, build links from
|
|
92
|
+
`SCRIPT_NAME`.
|
|
93
|
+
- **Links are built from the discovered prefix**, plus `X-Forwarded-Prefix`
|
|
94
|
+
when a proxy stripped one, never from a hard-coded `/api/v1`.
|
|
95
|
+
- **Never mutate the host's `scope` or `environ`.** Copy first.
|
|
96
|
+
|
|
97
|
+
Pecan is the awkward one: it does not rewrite `PATH_INFO`, so the mount point
|
|
98
|
+
is recovered by taking the remainder off the end.
|
|
99
|
+
|
|
100
|
+
### Lifecycle, without lifespan
|
|
101
|
+
|
|
102
|
+
**A mounted ASGI app never receives the lifespan scope.** The host router
|
|
103
|
+
handles it at the root and does not forward it to a mount. So startup cannot
|
|
104
|
+
hang off lifespan alone, and `Meter` owns its own lifecycle instead:
|
|
105
|
+
|
|
106
|
+
1. **Explicit, and preferred:** `meter.start()` / `meter.stop()`, or use it as
|
|
107
|
+
a context manager. A host with a lifespan of its own should call these
|
|
108
|
+
from it, or from `AppConfig.ready()`.
|
|
109
|
+
2. **From lifespan**, when our ASGI app *is* the root app.
|
|
110
|
+
3. **Lazily**, on the first request, with an `atexit` stop.
|
|
111
|
+
|
|
112
|
+
All three are idempotent and reference-counted, so starting from a host
|
|
113
|
+
lifespan *and* a first request is harmless.
|
|
114
|
+
|
|
115
|
+
### Being a good citizen
|
|
116
|
+
|
|
117
|
+
- No module-level singletons. Every app is a factory over a `Meter`, so two
|
|
118
|
+
meters can coexist in one process.
|
|
119
|
+
- No CORS headers, no global exception handlers, no logging configuration, no
|
|
120
|
+
signal handlers. Those are the host's business.
|
|
121
|
+
- `HEAD` and `OPTIONS` answered correctly.
|
|
122
|
+
- An unknown path inside our own prefix returns a JSON 404 rather than
|
|
123
|
+
raising into the host.
|
|
124
|
+
|
|
125
|
+
### Sync core, async edge
|
|
126
|
+
|
|
127
|
+
Handlers are plain synchronous functions returning a `MeterResponse`, so WSGI
|
|
128
|
+
and synchronous Django call them directly. The ASGI adapter offloads the
|
|
129
|
+
blocking datasource calls with `asyncio.to_thread` -- standard library, no
|
|
130
|
+
`anyio` dependency -- and feeds streams from an async generator.
|
|
131
|
+
|
|
132
|
+
WebSockets are declined with a clean close rather than implemented: SSE
|
|
133
|
+
already carries the stream, and it works over WSGI too.
|
|
134
|
+
|
|
135
|
+
**The WSGI streaming caveat**, which deployments need to know up front: a
|
|
136
|
+
synchronous WSGI worker holds one thread for as long as an SSE connection
|
|
137
|
+
stays open. Use gevent or eventlet workers for streaming, or let clients poll
|
|
138
|
+
`/events?since_seq=` instead.
|
|
139
|
+
|
|
140
|
+
## 5. Multiple workers
|
|
141
|
+
|
|
142
|
+
N API workers each constructing a polling `Meter` means N pollers on the same
|
|
143
|
+
database. Two supported shapes:
|
|
144
|
+
|
|
145
|
+
- `Meter(poll=True)` for single-process or embedded use.
|
|
146
|
+
- `Meter(poll=False)` for API workers reading a store that a separate
|
|
147
|
+
`taskflow-meter collect` process keeps filled.
|
|
148
|
+
|
|
149
|
+
With `poll=False` over a taskflow backend there is no event history, so the
|
|
150
|
+
event and stream endpoints answer 501 and flow payloads omit their links --
|
|
151
|
+
rather than serving an empty stream that cannot be told apart from silence.
|
|
152
|
+
|
|
153
|
+
## 6. Testing
|
|
154
|
+
|
|
155
|
+
The suite is built on the principle that a monitoring library which is only
|
|
156
|
+
tested against mocks has tested its own assumptions. So it runs real engines
|
|
157
|
+
(serial and parallel), a real sqlite logbook read from a second process, real
|
|
158
|
+
Pecan, Flask, Django and FastAPI applications, a real HTTP server, a real
|
|
159
|
+
kombu broker and a real oslo.messaging bus, and real alembic migrations
|
|
160
|
+
checked against the models.
|
|
161
|
+
|
|
162
|
+
| Tree | Subject |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| `tests/unit/<pkg>/test_<mod>.py` | One module, mirroring the package |
|
|
165
|
+
| `tests/functional/` | A behaviour across modules, including the cross-process one |
|
|
166
|
+
| `tests/integration/` | Real engines, and the collector deployment end to end |
|
|
167
|
+
| `tests/conformance/` | The ASGI and WSGI callables, and all six hosts, comparing bytes |
|
|
168
|
+
|
|
169
|
+
A unit test module must mirror its target or `tox -e pep8` fails on the
|
|
170
|
+
test-tree check. Anything that does not target a single module belongs in one
|
|
171
|
+
of the sibling trees.
|
|
@@ -4,6 +4,9 @@ How to deploy it into an OpenStack service, and how to answer the two
|
|
|
4
4
|
questions people actually have: *how far along is this flow?* and *what is it
|
|
5
5
|
doing right now?*
|
|
6
6
|
|
|
7
|
+
For why it is built the way it is -- what taskflow does and does not record,
|
|
8
|
+
and the rules an embedded app has to obey -- see [design.md](design.md).
|
|
9
|
+
|
|
7
10
|
---
|
|
8
11
|
|
|
9
12
|
## 1. Point it at a taskflow backend
|
|
@@ -186,6 +189,41 @@ The queue is durable and declared on publish, so a flow that starts before the
|
|
|
186
189
|
collector does is not a lost run. Events are keyed on `(run_id, seq)`, so a
|
|
187
190
|
collector that reconnects and redelivers writes nothing twice.
|
|
188
191
|
|
|
192
|
+
### Or on the bus the service already has
|
|
193
|
+
|
|
194
|
+
Inside an OpenStack service, the notification bus is usually already
|
|
195
|
+
configured, already monitored, and already pointed at the right broker with
|
|
196
|
+
the right credentials. The oslo.messaging transport uses it rather than
|
|
197
|
+
opening a connection of its own:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
taskflow-meter collect --transport oslo-messaging --url rabbit://guest@broker// --store-url postgresql://user@host/meter
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from taskflow_meter.transports.oslo_messaging import OsloMessagingTransport
|
|
205
|
+
|
|
206
|
+
# No URL: the service's own transport_url decides.
|
|
207
|
+
publisher = OsloMessagingTransport(conf=CONF)
|
|
208
|
+
with attach(engine, publishers=[publisher]):
|
|
209
|
+
engine.run()
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
These go out as **notifications**, not RPC -- fire-and-forget fanout, which
|
|
213
|
+
costs the flow nothing when no collector is running. They carry the
|
|
214
|
+
`taskflow_meter.events` event type on the `taskflow-meter` topic, so a
|
|
215
|
+
listener that also hears other traffic can pick them out. Several collectors
|
|
216
|
+
sharing the default pool share one queue, and each notification is handled
|
|
217
|
+
once rather than once per collector.
|
|
218
|
+
|
|
219
|
+
**One difference from the AMQP transport, and it decides which to pick.** A
|
|
220
|
+
notifier cannot declare the collector's queue -- the listener owns it -- and a
|
|
221
|
+
broker drops what it has nothing to route to. So events published before any
|
|
222
|
+
collector has *ever* run are lost. Start the collector once before the first
|
|
223
|
+
flow and the queue is durable from then on; restarts cost nothing. If flows
|
|
224
|
+
genuinely run before any collector exists, use the AMQP transport, which
|
|
225
|
+
declares its queue on every publish.
|
|
226
|
+
|
|
189
227
|
Unlike the persistence datasource, which inherits taskflow's retention, this
|
|
190
228
|
store keeps what it is told until told otherwise:
|
|
191
229
|
|
|
@@ -195,7 +233,7 @@ store.prune(before=time.time() - 30 * 86400)
|
|
|
195
233
|
|
|
196
234
|
---
|
|
197
235
|
|
|
198
|
-
##
|
|
236
|
+
## 4. Watching from inside the process running the flow
|
|
199
237
|
|
|
200
238
|
Everything above reads what taskflow persisted. If you control the code that
|
|
201
239
|
runs the flow, attaching to the engine gets you two things reading cannot:
|
|
@@ -242,7 +280,7 @@ without limit would take the process down later for reasons nobody would
|
|
|
242
280
|
connect back to monitoring. A publisher that raises is counted and logged,
|
|
243
281
|
and the next batch is still attempted. None of it can fail a task.
|
|
244
282
|
|
|
245
|
-
##
|
|
283
|
+
## 5. How far along is this flow?
|
|
246
284
|
|
|
247
285
|
Every flow payload carries a `completion` between 0 and 1:
|
|
248
286
|
|
|
@@ -311,7 +349,7 @@ another process within one poll interval.
|
|
|
311
349
|
|
|
312
350
|
---
|
|
313
351
|
|
|
314
|
-
##
|
|
352
|
+
## 6. What is it doing right now?
|
|
315
353
|
|
|
316
354
|
`running_atoms` names the atoms currently executing. It is a list because
|
|
317
355
|
unordered and graph flows run several at once, and it is empty between two
|
|
@@ -375,7 +413,7 @@ Two events are not flow activity and are worth handling:
|
|
|
375
413
|
|
|
376
414
|
---
|
|
377
415
|
|
|
378
|
-
##
|
|
416
|
+
## 7. What the meter cannot tell you
|
|
379
417
|
|
|
380
418
|
Worth knowing before you go looking for it:
|
|
381
419
|
|