Give cross-cutting concerns an owner instead of a convention (#145)
El SDK CI - dev / build-and-test (push) Failing after 11m4s
El SDK CI - dev / build-and-test (push) Failing after 11m4s
This commit was merged in pull request #145.
This commit is contained in:
+188
-2
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user