docs(api): add API conventions, migrations, and README#231
Conversation
|
Overall the v1 direction here is solid: plural nouns, correct method semantics, RFC 9457 errors, split request/response schemas, jobs for async work, RSQL replacing the old boolean-DSL magic chars. All good moves, agree with merging that as the baseline. Where I think this needs another pass is the batch/bulk section, specifically re how it's meant to actually serve RecCeiver. batch vs reconcile
Proposal: add an actual Reconcile doesn't get to skip jobs/per-item results just because it's declarative. Still large-N (thousands of channels per IOC per our own README numbers), still partially-failable, and a failed item must not get tombstoned (it didn't disappear, it just failed validation), so reconcile needs per-item results plus sync-under-threshold/async-over-threshold, same machinery batch has today, just wrapped around a "desired state" body instead of an op-list. generic (non-reconcile) bulk, leaving this open Once reconcile absorbs RecCeiver: is there still a real need for generic bulk create/update/delete (admin tooling, migration scripts, whatever)? If yes, and it's small-N/interactive (our own actor table already has admin/curator as "occasional, deliberate"), I don't think that needs jobs or per-item results at all. Validate-then-write, atomic, single response and status. Simpler for callers than today's 200-with-embedded-422s. If there's actually a consumer doing large/failure-prone homogeneous bulk writes outside RecCeiver, that path needs per-item handling too, same as reconcile, just without tombstoning. Need to know if that consumer exists before deciding. 200 vs 207 vs job, the doc contradicts itself here Hard rules say "never 200 with an error body." Async section says "never... a 207 Multi-Status." Sync "mirrors ES Non-atomicity and the 200-status choice for batch are both justified by "follows Elasticsearch native concepts needed for reconcile For tombstoning to be safe I think we need, minimum:
Don't think we need IOC / RecCeiver / session to become their own CF resources though, scope just needs to be whatever unit appears/disappears together. RecCeiver owns getting that scoping right. CF just needs the opaque key plus generation guard. one more thing this forces us to decide now, not later: can one channel have multiple hosts? Separately from all of the above, PVs can be hosted by more than one IOC at once. Today CF pretends this can't happen, so when more than one recceiver/IOC reports the same name, whichever write lands last just silently clobbers the previous metadata. No visibility that it happened. This lands directly on the I think existence and metadata split cleanly here. Existence can aggregate now: channel exists if ≥1 source claims it, tombstone only when the last claim drops, that's just reference counting, solvable today. Metadata conflict is harder: two sources reporting different properties/tags for the same name doesn't aggregate cleanly, and I don't think we need to solve that now (whether we warn, reject, or just surface both isn't decided). But the identity question, keyed by name alone vs. some (name, ioc_id)-style composite, exact keying TBD, needs to at least be flagged as open now, because retrofitting single-owner -> multi-claim later is a lot more expensive than leaving room for it in the reconcile primitive from the start. |
I can swap to reconcile.
I still don't understand tombstoning, is this a feature inside of channelfinder? Or is this a client is pushing some extra metadata? Can you give an example of using just the normal update a single channel example?
urgl, I don't particularly want to tackle this as it requires doing an elastic migration. The channel name is the key in the channels 'table'. Adding a source_id (which I think would have to be Which completely changes the scope of this PR. Adding support for a source_id, which I think is pretty easily added on top instead, we modify the Channel Response and Request object with an optional field source_id. This would not break a v1 api. I think it does not need to be added now for this reason. |
| Channel search uses plain, named, documented query parameters: | ||
|
|
||
| - Name glob, page size, and sort: `GET /channels?name=SR*&size=50&sort=channel_name:asc`. | ||
| - Tag presence and property values are named parameters too — `tag=active`, `cell=2`. |
There was a problem hiding this comment.
There is some ambiguity here: What if I have a property named tag?
From what I read (e.g. https://en.wikipedia.org/wiki/Tombstone_(data_store)) it seems not obviously relevant in our case as we do not have a distributed database to deal with, but perhaps it is the correct approach regardless? |
ChannelFinder - recCeiver always just pushes what it has. Else it needs state/db. Good that you're up for swapping to reconcile. @simon-ess's "why do we need to create, update, update, and delete all at once?" on the batch example is the same question from the other end I think.
TBH I am not 100% sure if it is the correct term, but I think it covers it. What I mean by it is that CF alone is responsible for changing channels from active to inactive. So my intention is basically soft delete + retention/purge policy, and the important part is who owns that (CF). To answer the either/or directly: today it is a client pushing extra metadata. That's Example Say Today: recCeiver reads CF, works out that Proposed: recCeiver sends its current set for its scope,
I think we should separate API design from business logic design. What I think is important is that the API already now accommodates for this design. How exactly that is implemented and/or how dbs are migrated to support it is a separate issue. Not asking for the migration, and definitely not in this PR. But "add it on top later" only holds for the field, not for the semantics. Schema-wise you're right, optional And it's not really future work, it's load-bearing for reconcile being correct. With a single Doesn't mean solving it now. But reconcile's rule needs to be "deactivate when nothing claims it" rather than "deactivate when the recorded source dropped it", and v1 can implement that with one claim per channel as a documented limitation. Free to write down now, expensive once clients have built on one-source-per-channel being the actual rule. |
During the meeting we agreed that I will be adding a "preprocessor" SPI similar to the "processor" one that exists. So each site can add their own validation for channels... one of these can be a validator that detects if existing/active channels are there in channelfinder which have different hosts, it can then log or notify the appropriate entities to resolve the issue. I don't think we should allow multiple properties with the same name. |
|
Overall I really like this. RSQL queries would be great. I'm also thinking more about the duplicate PV names. I agree with @anderslindho that it is good to think about even if we don't address/support it fully in v1. I'm not saying this is practical but if PV name is the key and a PV can have multiple {
"name": "SR12C:SQSF1:Setpoint",
"properties": {
"Acc": "SR",
"Family": "SQSF",
"Field": "Setpoint",
"Sector": "12",
"Device": "1",
"Position": "187.277343",
"hostName": "ioc-host-a.example.local",
"iocName": "skewquads2",
"iocid": "192.168.20.11:45334",
"iocIP": "192.168.20.11",
"time": "2026-07-15 09:12:00.123456",
"caPort": "45502",
"recordType": "ao",
"recordDesc": "Current setpoint",
"archive": "Slow",
"Engineer": "jdoe",
"Location": "SoftIOC"
},
"pvStatus": "Active",
"conflict": {
"duplicateActiveSources": true,
"activeSourceIds": [
"recceiver-1.example.local:192.168.20.11:45334",
"recceiver-2.example.local:192.168.20.12:52110"
],
"note": "flattened properties above reflect one of these sources arbitrarily; do not treat as authoritative until resolved"
},
"tags": ["MML", "PS"],
"sources": [
{
"source_id": "recceiver-1.example.local:192.168.20.11:45334",
"pvStatus": "Active",
"last_seen": "2026-07-15T09:12:00Z",
"properties": {
"recceiverID": "recceiver-1.example.local",
"iocName": "skewquads2",
"hostName": "ioc-host-a.example.local",
"iocid": "192.168.20.11:45334",
"iocIP": "192.168.20.11",
"time": "2026-07-15 09:12:00.123456",
"caPort": "45502",
"recordType": "ao",
"recordDesc": "Current setpoint",
"archive": "Slow",
"Engineer": "jdoe",
"Location": "SoftIOC"
}
},
{
"source_id": "recceiver-2.example.local:192.168.20.12:52110",
"pvStatus": "Active",
"last_seen": "2026-07-15T09:10:45Z",
"properties": {
"recceiverID": "recceiver-2.example.local",
"iocName": "skewquads2-test",
"hostName": "ioc-host-b.example.local",
"iocid": "192.168.20.12:52110",
"iocIP": "192.168.20.12",
"time": "2026-07-15 09:10:45.987654",
"caPort": "45519",
"recordType": "ao",
"recordDesc": "Current setpoint (test IOC)",
"archive": "Slow",
"Engineer": "jdoe",
"Location": "SoftIOC"
}
},
{
"source_id": "recceiver-1.example.local:192.168.20.13:38821",
"pvStatus": "Inactive",
"last_seen": "2026-06-20T14:22:03Z",
"properties": {
"recceiverID": "recceiver-1.example.local",
"iocName": "skewquads2",
"hostName": "ioc-host-old.example.local",
"iocid": "192.168.20.13:38821",
"iocIP": "192.168.20.13",
"time": "2026-06-20 14:22:03.456789",
"caPort": "45506",
"recordType": "ao",
"recordDesc": "Current setpoint",
"archive": "Slow",
"Engineer": "jdoe",
"Location": "SoftIOC"
}
}
]
}Each source in this case is an IOC connection to a specific recceiver instance and includes all the properties that IOC reported. The top level properties is the flattened view for normal clients and includes properties manually added by non-recceiver clients. The top level pvStatus is an OR of all the source pvStatus fields and there is a conflict flag in case there are multiple active sources. There are some problems with this idea (including an IOC restart adding a new source every time) but throwing it out there to brainstorm. |
|
oh that json blob reminded me of an old complaint about CF here: all the property values are strings so things like position in the example above are hard to sort on unless you make all positions the same number of characters. We added leading zeros in those cases. Not sure if it can be addressed or makes sense but did come up here |
0cdfa93 to
72cdbf1
Compare
|
Changes:
|
anderslindho
left a comment
There was a problem hiding this comment.
Nice, this took on basically everything that I hoped for. Few things still open.
Biggest is source_id granularity. Mechanism is fine, but on the recommended recceiver_id:ioc_name: "IOC-name" is an ESS-ism, and, although it can be an example, it should not be the canonical form. And it might be useful to state the actual rule, which is that scope has to be the smallest thing that appears/disappears together. Same PV under multiple IOCs on one recCeiver only works if you scope per-IOC - then X has a claim from ioc-A and ioc-B and stays active till both drop. Scope per-recCeiver and one IOC dropping deactivates PVs the other still serves. So IMO state the granularity rule, and say same-PV-under-multiple-scopes is expected multi-claim, not an error.
Batch being gone is good, but it leaves no generic bulk primitive at all - bulk just means reconcile now. Anything non-recCeiver wanting high-volume writes (admin, migration) is stuck looping singles or faking a source_id. Completely fine for me, but maybe worth stating explicitly - your call.
Reconcile items reference properties/tags and an undefined one 422s per-item, so a recCeiver pushing a new property has to define it first or those items fail. That pre-provision step cuts against one-way. Should reconcile create missing definitions, or do producers pre-provision and we document it?
conflict:true is a good call. But: you can see two sources disagree but not what the losing value was (probably ok since we dropped per-source props but might warrant call-out), and doesn't say when conflict clears.
Minor: source_id optional with host fallback reintroduces the host instability we're avoiding, for whoever doesn't set it. Might be worth making required.
None of this blocks merge for me.
72cdbf1 to
28a9617
Compare
That's why I made it recommended, its up to the producer to decide but something per-IOC is best. And something that survives reconnects of IOCs, which iocid does not. But I'll try make language clearer about what I mean by 'recommended'.
I will add produces pre-provision. This is how recceiver works now, and since property list is small I don't see a big problem with checking properties exist or just pushing the properties at start.
I might just drop this, multiple sources always suggests a conflict of some kind. Could just be latest time seen.
Updated the fallback to be based on iocname and iocid property + the host
|
28a9617 to
6050826
Compare
|
At our site we sometimes struggle with the duplicate IOCs being started on the same host and You also then lose the ability to see the history of a PV moving from IOC-A to IOC-B. Hmm. I'm not sure we would want the purge timeout here but I assume that can be disabled/set to 0. If a PV goes inactive because it was retired, we want that PV to stay in CF for a while in case people search for it. |
I'm not sure I follow here. We have hosts with multiple IOCs, so how would you know which one to remove when a new one appears? Would it be possible to have the reccasters send an optional |
| `current_generation`. This turns "fast clock wins forever" into "fast clock wins | ||
| this round; the loser re-syncs on its next snapshot." | ||
|
|
||
| Two redundant producers sharing one `source_id` is a deliberate choice, not a |
There was a problem hiding this comment.
When will two producers share the same source ID? Does that mean that the recceivers are configured with the same ID and they both find an IOC with the same name/id?
There was a problem hiding this comment.
Yes exactly. Two recceivers on same subnet, you might want them as a redundant pair. One gets restarted while the other stays up, the source_id stays the same and conflicts are handled by the generation param.
6050826 to
0728182
Compare
Document API conventions including RSQL query syntax, bulk operations, jobs/batch/source definitions, and migration notes under dev-docs/api/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0728182 to
de21dd2
Compare
|
The case I am thinking of is PV With iocid there is the issue of an IOC restart creating a new My question is can we make this workable, i.e. what about if a new active source appears, the inactive sources get purged automatically? Then IOC restarts don't continuously increase the number of sources. |



Document API conventions including RSQL query syntax, bulk operations, jobs/batch definitions, and migration notes under dev-docs/api/.