Give cross-cutting concerns an owner instead of a convention (#145)
El SDK CI - dev / build-and-test (push) Failing after 11m4s

This commit was merged in pull request #145.
This commit is contained in:
2026-08-16 16:57:51 +00:00
8 changed files with 664 additions and 44 deletions
+188 -2
View File
@@ -43,6 +43,7 @@
#include <dlfcn.h> /* dlsym for http_set_handler fallback */
#include <unistd.h>
#include <fcntl.h>
#include <sys/file.h> /* flock — process-identity singleton (program block) */
#include <dirent.h>
#include <errno.h>
#include <pthread.h>
@@ -18736,11 +18737,196 @@ void log_warn(el_val_t msg_v) {
fprintf(stderr, "[WARN] %s\n", msg ? msg : "");
}
/* config — read a configuration value from the environment.
* Returns "" if the variable is not set (same as __env_get). */
/* ── Cross-cutting concerns: process identity and configuration ──────────────
*
* These back the `program` block (see lang/spec/language.md §18). Both concerns
* were previously conventions "check nothing is already running first",
* "remember the right default at every read site" and conventions is exactly
* what they failed as. Here they are mechanisms, injected by the compiler at
* the process boundary, so no call site has to remember anything.
*/
/* -- Process identity ------------------------------------------------------- */
/* The lock fd is deliberately never closed. Holding it open for the process
* lifetime is what makes the guarantee work: the kernel drops an flock when the
* owning process dies, including on SIGKILL and on crash. That is why this is an
* flock and not a bare pidfile there is no stale-lock state to clean up, and
* therefore no "delete the pidfile to get unstuck" ritual that would itself
* become a convention. */
static int el_singleton_fd = -1;
static char el_singleton_path[1024];
static const char* el_singleton_dir(void) {
const char* d = getenv("EL_SINGLETON_DIR");
if (d && *d) return d;
d = getenv("TMPDIR");
if (d && *d) return d;
return "/tmp";
}
/* el_singleton_acquire — claim exclusive process identity, or refuse to start.
* Compiler-injected as the FIRST statement of main() for any program whose
* `program` block declares `singleton:`. */
el_val_t el_singleton_acquire(el_val_t id_v) {
const char* id = EL_CSTR(id_v);
if (!id || !*id) return EL_NULL;
/* Sanitise the id into a filename. */
char safe[256];
size_t si = 0;
for (const char* p = id; *p && si + 1 < sizeof(safe); p++) {
char c = *p;
int ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.';
safe[si++] = (char)(ok ? c : '-');
}
safe[si] = '\0';
snprintf(el_singleton_path, sizeof(el_singleton_path),
"%s/el-singleton-%s.lock", el_singleton_dir(), safe);
int fd = open(el_singleton_path, O_RDWR | O_CREAT, 0644);
if (fd < 0) {
fprintf(stderr, "[el] FATAL: singleton '%s': cannot open lock file %s: %s\n",
id, el_singleton_path, strerror(errno));
exit(1);
}
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
/* Someone else holds it. Report WHO. A pid is actionable; "already
* running" is not — and the observed failure was precisely a stale
* process that `pkill -f` had silently failed to match, still answering
* probes while a fresh build was believed to be under test. */
char buf[64];
buf[0] = '\0';
ssize_t n = pread(fd, buf, sizeof(buf) - 1, 0);
if (n > 0) buf[n] = '\0';
long holder = strtol(buf, NULL, 10);
fprintf(stderr, "[el] FATAL: another instance of '%s' is already running", id);
if (holder > 0) fprintf(stderr, " (pid %ld)", holder);
fprintf(stderr, ".\n"
"[el] lock: %s\n"
"[el] Refusing to start a second instance against the same\n"
"[el] state. Stop the running one and VERIFY it is gone\n"
"[el] (ps -p <pid>) before retrying.\n",
el_singleton_path);
close(fd);
exit(1);
}
/* We own it. Record our pid so the next would-be starter can name us. */
if (ftruncate(fd, 0) != 0) { /* best effort — the lock is the guarantee */ }
char pidbuf[32];
int pn = snprintf(pidbuf, sizeof(pidbuf), "%ld\n", (long)getpid());
if (pn > 0) { ssize_t w = write(fd, pidbuf, (size_t)pn); (void)w; }
el_singleton_fd = fd; /* never closed, by design */
return EL_NULL;
}
/* -- Configuration ---------------------------------------------------------- */
#define EL_CONFIG_MAX 128
typedef struct {
char name[128];
char type[16];
char* value; /* resolved: env value, else default; NULL if unset */
int has_default;
int required;
} ElConfigEntry;
static ElConfigEntry el_config_tab[EL_CONFIG_MAX];
static int el_config_n = 0;
static int el_config_has_schema = 0; /* did this program declare one at all? */
static int el_config_is_int(const char* s) {
if (!s || !*s) return 0;
if (*s == '-' || *s == '+') s++;
if (!*s) return 0;
for (; *s; s++) if (*s < '0' || *s > '9') return 0;
return 1;
}
/* el_config_declare — record ONE configuration entry and resolve it now.
* The default lives here, in the declaration, and nowhere else. */
el_val_t el_config_declare(el_val_t name_v, el_val_t type_v, el_val_t def_v,
el_val_t has_default_v, el_val_t required_v) {
const char* name = EL_CSTR(name_v);
if (!name || !*name) return EL_NULL;
el_config_has_schema = 1;
if (el_config_n >= EL_CONFIG_MAX) {
fprintf(stderr, "[el] FATAL: more than %d config entries declared.\n", EL_CONFIG_MAX);
exit(1);
}
const char* type = EL_CSTR(type_v);
const char* def = (def_v == EL_NULL) ? NULL : EL_CSTR(def_v);
ElConfigEntry* e = &el_config_tab[el_config_n++];
snprintf(e->name, sizeof(e->name), "%s", name);
snprintf(e->type, sizeof(e->type), "%s", type ? type : "String");
e->has_default = (int)(long)has_default_v;
e->required = (int)(long)required_v;
/* Resolution order: environment wins, declaration supplies the fallback. */
const char* env = getenv(name);
if (env && *env) e->value = el_strdup_persist(env);
else if (e->has_default && def) e->value = el_strdup_persist(def);
else e->value = NULL;
return EL_NULL;
}
/* el_config_validate — check the whole schema at once, before main() runs.
* Reports EVERY problem, not just the first: a startup that fails one variable
* at a time costs one restart per variable. */
el_val_t el_config_validate(el_val_t program_v) {
const char* prog = EL_CSTR(program_v);
int bad = 0;
for (int i = 0; i < el_config_n; i++) {
ElConfigEntry* e = &el_config_tab[i];
if (!e->value) {
if (e->required) {
fprintf(stderr, "[el] config: %s is required but is not set "
"(no value in the environment, no default declared)\n", e->name);
bad++;
}
continue;
}
if (strcmp(e->type, "Int") == 0 && !el_config_is_int(e->value)) {
fprintf(stderr, "[el] config: %s is declared Int but its value is \"%s\"\n",
e->name, e->value);
bad++;
}
}
if (bad) {
fprintf(stderr, "[el] FATAL: program '%s' has %d invalid configuration "
"entr%s. Refusing to start.\n",
prog ? prog : "?", bad, bad == 1 ? "y" : "ies");
exit(1);
}
return EL_NULL;
}
/* config — read a configuration value.
*
* When the program declared a schema, that schema is authoritative: the value
* has already been resolved and validated at startup, so this is a lookup and
* NOT a place where a default gets decided. Reading a key that was never
* declared is a bug at the read site, and is reported as one that enforcement
* is what makes the declaration real rather than advisory.
*
* With no schema declared, behaviour is unchanged (plain getenv), so programs
* that have not migrated keep working. */
el_val_t config(el_val_t key_v) {
const char* key = EL_CSTR(key_v);
if (!key || !*key) return EL_STR("");
if (el_config_has_schema) {
for (int i = 0; i < el_config_n; i++) {
if (strcmp(el_config_tab[i].name, key) == 0) {
const char* v = el_config_tab[i].value;
return el_wrap_str(el_strdup(v ? v : ""));
}
}
fprintf(stderr, "[el] FATAL: config(\"%s\") is not declared in the "
"program block. Declare it there, with its default, or stop "
"reading it.\n", key);
exit(1);
}
const char* val = getenv(key);
if (!val) return EL_STR("");
return el_wrap_str(el_strdup(val));