Repository navigation
Conversation
Signed-off-by: Mitch Gaffigan <mitch.gaffigan@comcast.net>
NicoPiel
left a comment
There was a problem hiding this comment.
-
Empty request body breaks the database setup. In database mode, an empty body (a
nullproperty) gets saved before the code crashes. The database then holds a bad entry. On restart, loading the config map fails and the server runs with an empty map. The endpoint should reject an empty body or blank key with a 400. -
Keys containing
/can't be updated through the new/configurationMap/{key}path. -
"Atomic" only holds on one server. It relies on a lock inside one Java process. In database mode each server keeps its own copy of the map, loaded once. Several servers sharing one database would still overwrite each other's changes, the same problem the PR is meant to fix.
-
There's no matching delete endpoint, so removing an entry still means rewriting the whole map, with the same race.
-
The Configuration Map settings panel in the admin UI (SettingsPanelMap.java:127) still saves the whole map, so UI users still have the race.
This appears to be a bug of the pre-existing endpoint. @tonygermano opened #520 to track this. The same fix is applicable both endpoints, since the validation should catch both.
Path components should be escaped.
Agreed - that may be the subject of future work. That's a larger architectural change than this PR contemplates, and the lore I'm aware of is that two mirth servers do not play nicely on the same database.
Agreed - that may be the subject of future work.
Agreed - that may be the subject of future work. |
PUT /configurationMap/{key} now responds with 400 Bad Request for a
blank key, as the user servlet does for a blank username. In file
mode, saving the map silently drops blank keys while they stay in
memory, so a blank key would work until the next restart and then
disappear. The key parameter description also says that a / in a key
has to be encoded as %2F.
Add a test that setting one key keeps the other entries, both in memory
and in what is saved, and a smoke test for the blank key, since the
check is in the servlet.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Tony Germano <tony@germano.name>
PUT /configurationMap/{key} takes a single ConfigurationProperty as the
request body, so give it XML and JSON examples like the neighbouring
configuration map endpoints.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Tony Germano <tony@germano.name>
ConfigurationProperty.versionOf returns the lowercase hex SHA-256 of an entry's value, a unit separator (U+001F), and its comment, with a null value and a null or blank comment treated as empty, and the empty string for a missing entry. It depends only on the entry's contents, so a client can calculate it from what the server returned and send it back as a precondition without the API storing or returning a revision. The unit separator is used instead of NUL because some clients can't easily hold a NUL in a string, such as C strings and shell variables. It is a static method rather than a getter so that it doesn't show up as a property of ConfigurationProperty in the OpenAPI schema. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Tony Germano <tony@germano.name>
PUT /configurationMap/{key} takes an optional expectedVersion query
parameter. If it is set and doesn't match the current entry's version
from ConfigurationProperty.versionOf, nothing is changed and the
endpoint returns false. An empty string requires that the key doesn't
exist yet. Without the parameter the entry is updated unconditionally,
as before, and the endpoint returns true.
This lets two clients updating the same key detect a lost update
without storing a revision. Reporting it as a false result follows the
revision checks on channels, channel groups and code templates, which
return false instead of an error status. The check runs inside the
controller's synchronized update.
The smoke test runs the update against a live server, including
calculating the version from the entry the server returned.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Tony Germano <tony@germano.name>
tonygermano
left a comment
There was a problem hiding this comment.
Thanks, the per-key endpoint is a real improvement. Right now two writers changing different keys can erase each other, and doing the read-modify-write on the server under the controller's lock fixes that. Rather than send you a list of changes, I pushed four commits to the branch. Please look them over.
Blank keys and request body examples
- Reject blank keys. The endpoint now responds with 400 for a blank key, as
UserServletdoes for a blank username. That covers the blank-key half of @NicoPiel's first point. In file mode, saving drops blank keys while they stay in memory, so a blank key would work until the next restart and then disappear. Thekeydescription also says a/has to be encoded as%2F. - Request body examples. Added XML/JSON
@ExampleObjectrefs for the singleConfigurationPropertybody, like the neighbouring endpoints.
Stale-write check
Channels, code templates and their groups/libraries all refuse a stale save unless the caller passes override=true. The configuration map had nothing like that, so two writers on the same key still lost one update silently. A stored revision number doesn't fit, because file mode keeps the map in a hand-editable configuration.properties. The new commits use a content-based version instead:
ConfigurationProperty.versionOf(property)is the lowercase hex SHA-256 of the UTF-8 bytes ofvalue + "\u001F" + comment(a unit separator rather than NUL, which C strings and shell variables can't easily hold), with a null value or a null/blank comment treated as empty, and""for a missing entry. Blank comments count as empty because file mode saves them as null, so a restart doesn't cause false conflicts. Clients compute it from whatGET /configurationMapreturned, so the GET response doesn't change.PUT /configurationMap/{key}takes an optionalexpectedVersionquery parameter and now returns a boolean. If the version doesn't match, nothing is changed and it returnsfalse, the same wayupdateChannel,updateChannelGroupsandupdateCodeTemplatereport a stale revision. An empty string means "the key must not exist yet", and leaving it out keeps last-write-wins. The comparison ignores case, so clients whose hex encoders produce uppercase don't need to convert. The operation description spells out the algorithm for REST callers.- A query parameter is used instead of ETag/
If-Matchbecause theClientproxy only exposes response bodies, so Java callers could never read an ETag.
This changes the Client signature and return type, so it has to go in before the endpoint ships.
Tests
DefaultConfigurationControllerTest covers keeping the other entries and the version matching rules, and ConfigurationPropertyTest checks known hash values. A ConfigurationMapEntryTest smoketest covers what needs a live server: the blank key, how a null or empty expectedVersion is sent, a version calculated from the GET response, and a rejected update coming back as false. Claude ran it in both file and database mode.
As you said, #520 applies to both endpoints. I'll send the fix as a follow-up PR once this one merges.
🤖 Drafted with Claude Code
The current API does not permit atomic writes to configuration maps. Two unsynchronized writers overwrite each other's settings. Add an atomic write endpoint.
With current endpoint:
With new endpoint:
Blocking for #449