Skip to content

Commit f513d11

Browse files
committed
docs(ldif-producer): add detailed test and architecture documentation
1 parent 91ad14c commit f513d11

File tree

5 files changed

+217
-74
lines changed

5 files changed

+217
-74
lines changed

README.md

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ The repository does include a compose file to start things up quickly. It will
2020
start three services:
2121

2222
- `ldap-server` - The OpenLDAP server.
23-
- `ldap-notifier` - The Univention Directory Notifier.
23+
- `ldif-producer` - Generates a provisioning message from every LDAP write transaction, replaces notifier/listener
24+
- `ldap-notifier` - The Univention Directory Notifier. (deprecated, about to be removed)
2425
- `ldap-admin` - An instance of phpLDAPadmin as a web UI to access
25-
`ldap-server`.
26-
2726

2827
To set it up:
2928

@@ -32,27 +31,35 @@ To set it up:
3231

3332
2. Bring up the services by running:
3433

35-
```
36-
docker compose up
37-
```
34+
```sh
35+
docker compose up
36+
```
3837

39-
The web UI is by default available at <http://localhost:8001>.
38+
To always start with a fresh LDAP database,
39+
always build with the latest changes
40+
and check for mew remote images, run this optimized command:
4041

42+
```sh
43+
docker compose down -v && docker compose up --pull always --build \
44+
ldif-producer ldap-server nats
45+
```
4146

47+
The web UI is by default available at <http://localhost:8001>.
4248

4349
## Interacting with `ldap-server`
4450

4551

4652
From the command line if you have the required tools available:
4753

48-
```
54+
```sh
4955
ldapwhoami -H ldap://localhost:389 -x -D cn=admin,dc=univention-organization,dc=intranet -w univention
5056

5157
ldapsearch -H ldap://localhost:389 -x -D cn=admin,dc=univention-organization,dc=intranet -w univention -b dc=univention-organization,dc=intranet
5258
```
5359

60+
## Interacting with the `ldap-server`
5461

55-
## Interacting with the `ldap-notifier`
62+
## Interacting with the `ldap-notifier` (about to be deprecated)
5663

5764
One option is to connect the base listener to the running notifier, this does
5865
involve manual tweaking at the moment though. The process is roughly as follows:
@@ -61,9 +68,10 @@ involve manual tweaking at the moment though. The process is roughly as follows:
6168
via `docker compose`. Set the `.env.listener` according to your local
6269
containers.
6370

64-
6571
## Manually testing a full round trip
6672

73+
**deprecated** because the phpLDAPadmin doesn't have the necessary LDAP Controls.
74+
6775
The easiest way is to open phpLDAPadmin and change the description of the admin
6876
user.
6977

@@ -89,7 +97,6 @@ Have the `container-listener-base` and the services from this repository running
8997
## Linting
9098

9199
You may run the pre-commit linter as follows:
92-
93100
```
94101
docker compose run pre-commit
95102
```
@@ -130,7 +137,7 @@ as a comma-separated list of values found in the [OpenLDAP documentation](https:
130137

131138
The default is `ldap/debug/level: stats`.
132139

133-
## Notifier Data Files
140+
## Notifier Data Files (deprecated, will soon be removed)
134141

135142
### OpenLDAP translog output file
136143

docs/ldif_producer_activity_diagram.puml

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ fork
1212

1313
partition "Send Messages to NATS" {
1414
repeat
15-
:get message from queue;
16-
:send message to NATS;
15+
:get message from
16+
`outgoing_queue`;
17+
:send message to NATS
18+
into LDIF_PRODUCER queue;
1719
:remove message from journal;
1820
repeat while ( no SIGINT / SIGTERM )
1921
}
@@ -24,16 +26,15 @@ fork again
2426
fork
2527
repeat
2628
:listen for messages in the socket;
27-
:handle_request()
29+
:slapdsock.service
30+
.SlapdSockServer.handle_request()
2831
====
2932
handle_request simply puts
30-
messages in a python queue
33+
incoming socket messages into
34+
the `requests` python queue
3135
so that multiple threads
3236
can work on them.;
3337

34-
:slapdsock.service
35-
.SlapdSockServer.handle_request()
36-
put message into queue;
3738
repeat while ( no SIGINT / SIGTERM )
3839

3940
fork again
@@ -48,20 +49,24 @@ fork again
4849
and determine the ldap operation;
4950
switch (ldap_operation_type)
5051
case ( RESULT )
51-
partition "do_result()" {
52+
partition "LDAPHandler.\ndo_result()" {
5253
:write message to journal;
5354
:put message into `outgoing_queue`;
5455
:release back-pressure
55-
by removing an object from the `backpressure_queue`;
56+
by removing an object from
57+
the `backpressure_queue`;
5658
}
5759
case ( DELETE / MODIFY / MODRDN )
58-
:do_delete()
60+
:LDAPHandler.
61+
do_add()
62+
do_delete()
5963
do_modify()
6064
do_modrdn()
6165
====
62-
add object to `backpressure_queue`;
66+
add object to
67+
`backpressure_queue`;
6368
case ( Everything else )
64-
:do_add()
69+
:LDAPHandler.
6570
do_bind()
6671
do_search()
6772
...
@@ -73,12 +78,6 @@ fork again
7378
fork again
7479
partition "SocketServer\nWorker Thread" {
7580
}
76-
fork again
77-
partition "SocketServer\nWorker Thread" {
78-
}
79-
80-
end fork
81-
}
8281

8382
end fork
8483

docs/ldif_producer_activity_diagram.svg

Lines changed: 18 additions & 19 deletions
Loading

docs/ldif_producer_architecture.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# LDIF-Producer
2+
3+
The LDIF-Producer produces a provisioning message
4+
for every LDAP write transaction (`ADD`, `DELETE`, `MODIFY`, `MODRDN`)
5+
and pushes them into a NATS queue.
6+
7+
The messages are still in the LDAP Object representation.
8+
A separate component is responsible for converting the Object
9+
to it's UDM representation.
10+
11+
## High-level architecture
12+
13+
The LDIF-Producer combines two worlds.
14+
15+
The slapd-sock-server based on `socketserver`, which is itself `threading`-based.
16+
And the official NATS python client library which is `asyncio`-based.
17+
18+
The socketserver main thread and the asyncio even loop
19+
run in separate threads and are connected via the `outgoing_queue`
20+
synchronous python queue.
21+
The socketserver internally uses two additional python queues.
22+
They will be explained later.
23+
24+
the ldif_producer.controller python module
25+
configures and starts both components,
26+
owns the `outgoing_queue` and initializes a graceful shutdown
27+
via a `signal_handler`.
28+
29+
## slapd-sock-server
30+
31+
the LDIF-Producer hooks into the LDAP-Server
32+
via the back-sock LDAP overlay module.
33+
It provides Pre-Transaction and Post-Transaction hook capabilities
34+
via a UNIX TCP Socket.
35+
One problem with this approach is, that LDAP requests
36+
can only be blocked in the Pre-hook (ADDRequset, DELETERequest, MODIFYRequest...)
37+
and only the Post-hook (RESULTRequest)
38+
provides the necessary LDAP object / LDIF data
39+
to construct the provisioning message.
40+
41+
This leads to the requirements that:
42+
43+
1. the Post-hook must locally persist the message content as soon as possible.
44+
2. The first action after startup must be to check the transaction journal
45+
and send any remaining messages to NATS.
46+
3. A post-hook handler function must never be aborted before it persisted
47+
it's messages to the transaction journal.
48+
4. modify requests need to be throtteled in the Pre-hook
49+
to not overload the downstream tasks.
50+
51+
Requirements one and two are not yet implemented
52+
in the first alpha version of this component.
53+
But it's already prepared in the `LDAPHandler.do_result()` function.
54+
Requirement three is implemented via a signal_handler
55+
that propagates an exit event to all threads and asyncio tasks
56+
and careful tuning of the python queue and socket polling timeout
57+
so that the exit event is evaluated with a reasonable frequency.
58+
(about once per second)
59+
Requirement four is implemented through a python `backpressure_queue`.
60+
The pre-hooks add an object to the `backpressure_queue` for ever LDAP write request
61+
and the post-hook removes an item after it has processed the request.
62+
While processing the request, the message is added to the `outgoing_queue`.
63+
That queue has a maximum size and blocks adding an item if it is reached.
64+
Items are removed from the `outgoing_queue`
65+
only after being successfully sent to NATS.
66+
This way, a slow/unresponsive NATS
67+
slows/stops the LDAP server from processing write requests.
68+
The pre-hook waits for a configurable timeout
69+
for a `backpressure_queue` seat to become available,
70+
before cancelling the LDAP Request with a custom error:
71+
72+
```text
73+
RESULT
74+
code: 51
75+
matched: <DN>
76+
info: slapdsocklistener busy sending messages to the message queue\n
77+
```
78+
79+
Code 51 = `LDAP_BUSY`
80+
81+
### Classes and threads
82+
83+
The LDIF-Producer acts as the UNIX Socket server
84+
while the back-sock LDAP overlay module is the client.
85+
86+
The LdifProducerSlapdSockServer or rather it's parent classes
87+
create and own the UNIX socket and the `requests` python queue.
88+
It is instantiated in the main controller and executed
89+
as a blocking and long-running process
90+
in the socketserver main thread.
91+
It listens for TCP requests on the socket and puts them into the `requests` queue.
92+
The socketserver main thread also spawns worker threads
93+
that consume the `requests` queue.
94+
95+
The logic for how to respond to back-sock hook requests
96+
is defined in the LDAPHandler class and it's parents.
97+
It is instantiated as a signleton,
98+
owns and instantiates the `backpressure_queue`
99+
and recieves a reference to the `outgoing_queue`
100+
from the main controller.
101+
102+
## Performance Considerations
103+
104+
Multiple slapd-sock worker threads can and should be configured
105+
to quickly respond to the LDAP-Server.
106+
107+
But the `backpressure_queue` is currently hard-coded to 1
108+
This configuration means that many read requests
109+
can be handeled concurrently, but only one write-request
110+
can happen at a given time.
111+
This choice was made because we currently don't know
112+
how to preserve the message order
113+
with multiple in-flight write transactions.
114+
115+
I expect that the bottleneck will be sending messages to NATS
116+
but don't know how to parallelize this operation
117+
without compromising the message order.
118+
119+
## Multi-Master LDAP-Server support
120+
121+
still under discussion.
122+
We expect that queue deduplication by the NATS server
123+
based on a unique transaction id
124+
will do the heavy lifting.

tests/README.md

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# Test Organization
22

3-
43
## TL;DR run the tests
54

65
In a container:
@@ -11,51 +10,66 @@ docker compose up --build test
1110

1211
Locally:
1312

14-
```shell
15-
# Use "pipenv" to have the right environment
16-
pipenv sync -d
17-
pipenv run pytest
18-
19-
# Get a shell
20-
pipenv shell
13+
```sh
14+
poetry install
15+
poetry shell
16+
pytest --cov=univention/provisioning/ --cov-report term-missing -v
2117
```
2218

23-
2419
## Target structure
2520

2621
The target structure for testing shall eventually follow this pattern:
2722

2823
```
29-
└── tests # top-level tests folder
30-
├── README.md # explains test organization inside this folder
31-
├── unit # name this unit_and_integration if keeping both here
32-
├── integration # optional: omit if kept together with unit tests
24+
└── tests
25+
├── README.md
26+
├── unit
27+
├── integration
3328
└── e2e
3429
```
3530

3631
### Unit tests
3732

38-
The *unit* under test is in this repository the "container":
33+
Unit tests can run without any external dependencies.
3934

40-
- `ldap-server` is the OpenLDAP server
35+
The unittests in this repository test the python code
36+
of the the LDIF-Producer slapd-socket server.
4137

42-
- `ldap-notifier` is the Univention Directory Notifier
38+
the LDIF-Producer is a multithreaded application
39+
using the python threading library.
40+
Due to this fact, some unittests take around a second to execute
41+
or more explicitly to shut down
42+
because the exit signal can only be evaluated
43+
between polling intervals.
4344

44-
Simple checks which focus on a single container are kept in the subdirectory
45-
`unit`.
45+
The multithreaded nature of the LDIF-Producer
46+
makes debugging test failures significantly harder.
47+
Tests can seem to be "hanging forever"
48+
when a worker thread throws an exception.
4649

50+
The LDIF-Producer implements extensive logging to help in debugging.
51+
In a pytest run, you can activate the logs with the `-s` and `--full-trace`
52+
cli arguments.
4753

48-
### Integration tests
54+
As a last resort, you can separate `stdout` from `stderr`
55+
you can sort the logs by process id to reveal different patterns.
56+
57+
```sh
58+
docker logs dev-local-ldif-producer-1 > ldif-producer.log \
59+
&& docker logs dev-local-ldif-producer-1 2>> ldif-producer.log \
60+
&& vim ldif-producer.log
61+
```
4962

50-
The current integration tests are in the folder `integration-test` in the root
51-
of this repository. The aim is to migrate those eventually into the folder
52-
`integration` as shown above in the overview.
63+
vim sort command:
64+
`:sort /\[\d \d*\]/`
5365

54-
Tests which check the *integration* of multiple containers will be grouped into
55-
the folder `integration` as shown above in the overview.
66+
### Integration tests
5667

57-
New tests are written as plain `pytest` based test cases.
68+
Integration tests are all test that depend on separately started containers.
69+
Those can be the `ldap-server`, `ldif-producer` and `nats`.
70+
Integration tests may need only one, some or all of those containers
5871

72+
the tests in the `integration-tests` folder are assumed to be deprecated.
5973

6074
### End to end tests
6175

0 commit comments

Comments
 (0)