feat: conditional compilation, first feature — REGISTRATIONS builds out end to end - #65
Conversation
…ut end to end
The COR_FEATURE_* options have emitted -D flags since they were added, and no
source read one: `grep -c "ifdef COR_FEATURE" src` was 0. The flags were a
table of intentions. This makes the first of them real.
`-DCOR_FEATURE_REGISTRATIONS=OFF` now builds, links, starts and passes its
suite. It drops the Context Source Registration, registration-subscription and
EntityMap service routines, the forwarding library and both DB plugins'
registration code: 23,936 bytes off .text.
WHAT A COMPILED-OUT ENDPOINT ANSWERS
The fifteen routes keep their table entry and lose their handler, through a
macro that DISCARDS its argument:
#if COR_FEATURE_REGISTRATIONS
# define REGS(handler) handler
#else
# define REGS(handler) corNotInThisBuild
#endif
so the handler symbol is never referenced and its .c can leave the build. That
is what makes the binary shrink; gating only the routing would keep every
handler linked in.
The entry stays so the URL still MATCHES, and the answer is 501, not the 404 an
absent route would give. A 404 says the resource is not there and invites the
client to fix its URL; 501 with the verb and path says the deployment declined
the capability, and the client's move is a different deployment. TS 104-176
§ 6.3.2 registers no error type for that — the one 501 in the table,
NoMultiTenantSupport, is reserved for a single capability — so the type URI is
ours, carrying the name spec-doubt #124 proposes. One #define changes the day
it is registered.
TWO THINGS THE PLAN HAD NOT ACCOUNTED FOR
The DB plugins needed the same treatment: mongoc.so failed dlopen with
"undefined symbol: mongocRegistrationUpdate", because its driver table
assigned five functions its own CMakeLists had just excluded. The slots now
stay NULL, which is the existing convention and which every db.registration*
call site already checks. And forwardingHttpRegister() in coraine.c.
#if, NOT #ifdef
CMakeLists now defines every feature to 1 or 0 and never leaves one absent, and
-Wundef is on: a misspelt feature name is a compile error rather than a
silently-false test that quietly compiles the code out. The same 0/1 makes a
flag usable as a C value, which is how coraineFeatures.c reports the set
without fifteen #ifdefs.
ASKING A BINARY WHAT IT IS
`coraine --version` gains a "features: NAME=0|1 ..." line and GET /version a
"features" object — a build with an endpoint compiled out is otherwise
indistinguishable from a full one, and a client that gets a 501 can now read
the reason instead of guessing. Every feature is listed with its value; a
missing name would not distinguish "off" from "too old to know the name".
The suite reads the same line (corTestParams.sh) and turns it into
COR_TEST_FEATURES for corTest's REQUIRE_FEATURE / SKIP_FEATURE. 184 of the 640
cases need REGISTRATIONS and leave the run set on a build without it;
registrations_not_in_this_build.test carries SKIP_FEATURE and is the one that
can only run there. It pins all fifteen 501s, that an unknown URL is still 404,
and that request validation still runs FIRST — a body that does not parse gets
its 400 on a compiled-out endpoint too, so the 501 is the answer to a
well-formed request rather than a blanket answer to the URL. Its last step is
the point of the exercise: everything not gated by the feature still works.
full build 640 tests, 640 passed
REGISTRATIONS=OFF 457 tests, 457 passed
The 457 is 640 - 184 + 1, and nothing outside the 184 failed.
`make di CMAKE_FEATURES=-DCOR_FEATURE_REGISTRATIONS=OFF BUILD_DEBUG=<dir>`
builds a reduced tree without turning the ordinary one into it — CMake caches
what it is given, per build DIRECTORY.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGatXwrHx1CreL49sCuS37
The features object went onto /version because that was the endpoint that
existed. It does not belong there: /version answers "what am I talking to" and
is the small stateless health probe it was designed as, and the feature set is
the answer to a different question. So /version is back to product, version and
the linked-library commits — byte for byte what it was — and the new endpoint
answers the other one properly.
GET /build
{
"product": "coraine", "version": "0.4.0",
"build": { "gitSha": …, "builtAt": …, "type": "Debug", "compiler": "GNU 15.2.0" },
"features": { "REGISTRATIONS": false, … },
"plugins": { "directory": …, "built": [ … ], "loaded": { … } },
"runtime": { "distributed": false, "splitEntities": true, "httpEndpoint": … }
}
THREE GROUPS, BECAUSE THEY CHANGE AT THREE DIFFERENT MOMENTS
That is the whole design, and it is what an operator chasing "why does this
deployment not do X" needs to know before deciding where to look:
features fixed when the source was compiled. If REGISTRATIONS is false, no
flag, plugin or restart brings the registry API back.
plugins `built` is compile-time too, `loaded` is decided at startup from a
directory this binary does not own.
runtime the command line, changed by a restart.
The last group earns its place: a broker with REGISTRATIONS compiled IN and
--distributed off accepts registrations and forwards nothing. Two separate
switches, and this is the only place both are visible at once.
BUILT IS NOT INSTALLED
`built` means the cmake run that produced this binary also produced that plugin
— not that the file is on disk. They are separate .so files loaded from a
directory the broker does not own, so a build can carry a plugin nobody
installed and an install can hold one from another build. The directory is
reported so `ls` answers the other half; scanning it from C would have reported
the INSTALL under a key that reads like the build.
NOT UNDER /admin
The admin API is itself a compile-time feature. An endpoint that reports what a
build contains must not be one of the things a build can leave out.
WHERE EACH FACT COMES FROM
Only cmake knows the flavour and the compiler it chose, so it passes those as
-D. Only the makefile knows the sha and the moment, so it generates
coraineBuild.h — the sibling of coraineStack.h, split from it because "what is
linked in" and "where this binary came from" are different questions. Neither
guesses at the other's half.
--version keeps its features line: the test harness needs an answer before a
broker starts, and that is the one question a broken installation still has to
be able to answer.
TEST
build_endpoint.test restarts the broker with --distributed --apiPlugins admin
and asserts the report follows. Without that step `runtime` could be four
constants and nothing would notice. registrations_not_in_this_build.test now
asks /build for its own REGISTRATIONS: false.
full build 641 tests, 641 passed
REGISTRATIONS=OFF 458 tests, 458 passed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGatXwrHx1CreL49sCuS37
|
Added
GET /build
{
"product": "coraine", "version": "0.4.0",
"build": { "gitSha": "…", "builtAt": "…", "type": "Debug", "compiler": "GNU 15.2.0" },
"features": { "REGISTRATIONS": false, … },
"plugins": { "directory": "/opt/seamware/plugins", "built": [ … ], "loaded": { … } },
"runtime": { "distributed": false, "splitEntities": true, "httpEndpoint": "…" }
}Three groups, because they change at three different moments — which is what an operator chasing "why does this deployment not do X" needs to know before deciding where to look. A feature is fixed when the source was compiled;
Not under Each fact comes from whoever knows it — cmake passes the flavour and compiler as
The earlier red CI was the ordering: that run started nine hours before corTest#2 merged, so |
The
COR_FEATURE_*options have emitted-Dflags since they were added, and no source read one —grep -c "ifdef COR_FEATURE" srcwas 0. The flags were a table of intentions. This makes the first of them real.-DCOR_FEATURE_REGISTRATIONS=OFFnow builds, links, starts and passes its suite. It drops the Context Source Registration, registration-subscription and EntityMap service routines, the forwarding library and both DB plugins' registration code: 23,936 bytes off.text.What a compiled-out endpoint answers
The fifteen routes keep their table entry and lose their handler, through a macro that DISCARDS its argument:
so the handler symbol is never referenced and its
.ccan leave the build. That is what makes the binary shrink; gating only the routing would keep every handler linked in.The entry stays so the URL still matches, and the answer is 501, not the 404 an absent route would give. A 404 says the resource is not there and invites the client to fix its URL; 501 with the verb and path says the deployment declined the capability, and the client's move is a different deployment.
{ "type": "https://coraine.readthedocs.io/errors/NotAvailableInThisDeployment", "title": "Not Available In This Build", "status": 501, "detail": "'POST /ngsi-ld/v1/csourceRegistrations' is not included in this build of coraine" }TS 104-176 § 6.3.2 registers no error type for that — the one 501 in the table,
NoMultiTenantSupport, is reserved for a single capability — so the type URI is ours, carrying the name spec-doubt #124 proposes. One#definechanges the day it is registered.Two things the plan had not accounted for
The DB plugins needed the same treatment:
mongoc.sofaileddlopenwithundefined symbol: mongocRegistrationUpdate, because its driver table assigned five functions its own CMakeLists had just excluded. The slots now stay NULL — the existing convention, and everydb.registration*call site already checks it. AndforwardingHttpRegister()incoraine.c.#if, not#ifdefCMakeLists now defines every feature to 1 or 0 and never leaves one absent, and
-Wundefis on: a misspelt feature name is a compile error rather than a silently-false test that quietly compiles the code out. The same 0/1 makes a flag usable as a C value, which is howcoraineFeatures.creports the set without fifteen#ifdefs.Asking a binary what it is
and
GET /versiongains a matchingfeaturesobject — a build with an endpoint compiled out is otherwise indistinguishable from a full one, and a client that gets a 501 can read the reason instead of guessing. Every feature is listed with its value; a missing name would not distinguish "off" from "too old to know the name".The suite reads the same line and turns it into
COR_TEST_FEATURESfor corTest'sREQUIRE_FEATURE/SKIP_FEATURE(SEAMWARE/corTest#2). 184 of the 640 cases needREGISTRATIONS;registrations_not_in_this_build.testcarriesSKIP_FEATUREand is the one that can only run without it. It pins all fifteen 501s, that an unknown URL is still 404, and that request validation runs first — a body that does not parse gets its 400 on a compiled-out endpoint too, so the 501 is the answer to a well-formed request rather than a blanket answer to the URL. Its last step is the point of the exercise: everything not gated by the feature still works.Test
REGISTRATIONS=OFF457 = 640 − 184 + 1, and nothing outside the 184 failed.
make di CMAKE_FEATURES=-DCOR_FEATURE_REGISTRATIONS=OFF BUILD_DEBUG=<dir>builds a reduced tree without turning the ordinary one into it — CMake caches what it is given, per build directory.
🤖 Generated with Claude Code
https://claude.ai/code/session_01TGatXwrHx1CreL49sCuS37