Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / src / virt / v-libvirt.c
Author[]Andy Green <andy@warmcat.com> 2026-09-25 02:05 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-09-25 02:05 UTC
Tree3fff63341ca16eb0a0af5e7d1c439d47933d5412   Raw Patch
 
builder: hold a task's job dir across the gap between its steps
builder: hold a task's job dir across the gap between its steps

A task's build steps are each offered, accepted, run and destroyed
separately, so between one step finishing and the next being offered
there is no nspawn pointing at the job dir.  The deletion paths only
spare dirs that have a live nspawn, so a job dir in that gap was fair
game, and the next step then started with its src/ tree gone:

  /home/sai/jobs/F302093F//sai-build-script.sh: line 18: cd:
      /home/sai/jobs/F302093F/src: No such file or directory

Take a hold on the job dir instead, renewed at every step and dropped
when the task finishes, fails or is cancelled, with a two hour expiry as
a backstop for a task that vanished server-side.  scan_jobs_dir_cb()
spares held dirs.  Failed job dirs are deliberately left for inspection,
so a failure drops the hold and lets the usual age rules have them.

The hourly cleanup would not have taken such a dir anyway (24h age gate,
and git_helper.sh is rewritten every step so the mtime stays fresh), but
saib_deletion_free_kib() deletes dirs of any age, so give it its own
30 minute floor and never let it take the job dir of the very task it is
being asked to make room for.

What made that fire in the first place is the reserved-resource
accounting: the ram/disk reservation was added only on the success path
of saib_consider_allocating_task() but given back for every destroyed
nspawn with a task, so a single failure during setup underflowed the
uint64 counters.  needed_disk (cast through unsigned int) was then
absurd, every offer was refused for lack of disk, and every refusal
called free_kib() with a huge need, which clamped to_delete to every
young inactive job dir on the builder.  Record on the nspawn what it
actually reserved and give back exactly that, clamped, and do the disk
arithmetic in 64 bits throughout.

Also say in the task's log why a task was refused for ram or disk, rate
limited to one line per task per minute since the server re-offers, and
ask the deletion stub for the job dir name rather than the full task uuid
on the cancel path -- job dirs are named from the uuid, so asking for the
uuid silently removed nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
diff --git a/src/builder/b-deletion.c b/src/builder/b-deletion.c index 7a9ffbd..8eb7f30 100644 --- a/src/builder/b-deletion.c +++ b/src/builder/b-deletion.c @@ -398,6 +398,109 @@ sai_deletion_worker(const char *home_dir_unused) * We are careful not to delete anything that is part of an ongoing job. */ +/* + * Job dir holds + * + * A task's build steps are each offered, run and destroyed separately, so + * between one step finishing and the next being offered there is no nspawn + * pointing at the job dir. The live-nspawn scan below is therefore not enough + * to protect a job dir of a build that is still going: a hold covers the gap. + * + * Holds are renewed at every step and dropped when the task finishes or fails, + * with a generous expiry as a backstop for a task that vanished server-side. + */ + +static saib_jobdir_hold_t * +saib_jobdir_hold_find(const char *vn) +{ + lws_start_foreach_dll(struct lws_dll2 *, p, builder.jobdir_hold_owner.head) { + saib_jobdir_hold_t *h = lws_container_of(p, saib_jobdir_hold_t, list); + + if (!strcmp(h->vn, vn)) + return h; + + } lws_end_foreach_dll(p); + + return NULL; +} + +void +saib_jobdir_hold(const char *vn) +{ + saib_jobdir_hold_t *h; + + if (!vn || !vn[0]) + return; + + h = saib_jobdir_hold_find(vn); + if (!h) { + h = malloc(sizeof(*h)); + if (!h) + return; + memset(h, 0, sizeof(*h)); + lws_strncpy(h->vn, vn, sizeof(h->vn)); + lws_dll2_add_tail(&h->list, &builder.jobdir_hold_owner); + lwsl_info("%s: holding job dir %s\n", __func__, vn); + } + + h->renewed = (uint64_t)lws_now_secs(); +} + +void +saib_jobdir_release(const char *vn) +{ + saib_jobdir_hold_t *h; + + if (!vn || !vn[0]) + return; + + h = saib_jobdir_hold_find(vn); + if (!h) + return; + + lwsl_info("%s: releasing job dir %s\n", __func__, vn); + lws_dll2_remove(&h->list); + free(h); +} + +int +saib_jobdir_is_held(const char *vn) +{ + saib_jobdir_hold_t *h = saib_jobdir_hold_find(vn); + uint64_t now; + + if (!h) + return 0; + + now = (uint64_t)lws_now_secs(); + + if (now > h->renewed && + now - h->renewed > SAI_JOBDIR_HOLD_MAX_SECS) { + lwsl_notice("%s: hold on job dir %s expired after %llus\n", + __func__, vn, + (unsigned long long)(now - h->renewed)); + lws_dll2_remove(&h->list); + free(h); + + return 0; + } + + return 1; +} + +void +saib_jobdir_holds_destroy(void) +{ + lws_start_foreach_dll_safe(struct lws_dll2 *, p, p1, + builder.jobdir_hold_owner.head) { + saib_jobdir_hold_t *h = lws_container_of(p, saib_jobdir_hold_t, list); + + lws_dll2_remove(&h->list); + free(h); + + } lws_end_foreach_dll_safe(p, p1); +} + struct inactive_job { struct inactive_job *next; char name[32]; @@ -451,6 +554,18 @@ scan_jobs_dir_cb(const char *dirpath, void *user, struct lws_dir_entry *lde) } lws_end_foreach_dll(p); + /* + * A task between two of its steps has no live nspawn, so it will not be + * on the active list above... but its job dir still holds the tree the + * next step is going to build in + */ + + if (saib_jobdir_is_held(lde->name)) { + lwsl_info("%s: %s is held (task still building)\n", __func__, + lde->name); + return 0; + } + lws_snprintf(path, sizeof(path), "%s/%s", dirpath, lde->name); if (stat(path, &sb)) { lwsl_notice("%s: stat failed %s: errno %d (%s)\n", __func__, path, errno, strerror(errno)); @@ -499,8 +614,16 @@ scan_jobs_dir_cb(const char *dirpath, void *user, struct lws_dir_entry *lde) return 0; } +/* + * Try to get \p needed_kib available under <home>/jobs by removing job dirs. + * + * \p protect_vn, if given, is the job dir of the task this is being done on + * behalf of: it must survive even though it has no live nspawn, since its + * earlier steps' output is what the next step builds on. + */ + int -saib_deletion_free_kib(unsigned int needed_kib) +saib_deletion_free_kib(unsigned int needed_kib, const char *protect_vn) { struct sai_builder *b = &builder; struct cleanup_ctx ctx; @@ -529,27 +652,63 @@ saib_deletion_free_kib(unsigned int needed_kib) } lws_end_foreach_dll_safe(d2, d3); } lws_end_foreach_dll_safe(d, d1); + /* + * Add the dir we must not touch to the active list: the scan spares + * anything on it, and it needs no other special-casing + */ + + if (protect_vn && protect_vn[0]) { + struct active_job_uuid *aj = lwsac_use_zero(&ctx.ac, sizeof(*aj), 64); + + if (aj) { + lws_strncpy(aj->uuid, protect_vn, sizeof(aj->uuid)); + lws_dll2_add_tail(&aj->list, &ctx.active_owner); + } + } + lws_snprintf(path, sizeof(path), "%s/jobs", b->home); lws_dir(path, &ctx, scan_jobs_dir_cb); if (ctx.inactive_count) { - int n, to_delete = 1; + int n, to_delete = 1, candidates = 0; struct inactive_job **sorted, *ij; - /* Assume each job frees roughly 100MB to reduce ping-ponging */ - to_delete = (int)(needed_kib - free_kib) / (100 * 1024); - if (to_delete < 1) to_delete = 1; - if (to_delete > ctx.inactive_count) to_delete = ctx.inactive_count; - sorted = lwsac_use(&ctx.ac, sizeof(*sorted) * (unsigned int)ctx.inactive_count, 0); if (sorted) { - n = 0; ij = ctx.inactive_head; while (ij) { - sorted[n++] = ij; + /* + * Unlike the periodic cleanup, we delete dirs + * that are not yet a day old, so we need our + * own floor: something this fresh belongs to a + * build that is still going on (a task waiting + * for its next step to be offered, say) and + * taking it just breaks that build instead of + * fixing our disk problem. + */ + if (ij->age >= SAI_FREEKIB_JOB_DIR_MIN_AGE_SECS) + sorted[candidates++] = ij; + else + lwsl_info("%s: sparing %s, only %llus old\n", + __func__, ij->name, + (unsigned long long)ij->age); ij = ij->next; } - qsort(sorted, (size_t)ctx.inactive_count, sizeof(*sorted), compare_age); + + if (!candidates) { + lwsl_warn("%s: need %uMiB, only %uMiB free, but no " + "job dir is old enough to remove\n", + __func__, needed_kib / 1024, + free_kib / 1024); + goto done; + } + + /* Assume each job frees roughly 100MB to reduce ping-ponging */ + to_delete = (int)(needed_kib - free_kib) / (100 * 1024); + if (to_delete < 1) to_delete = 1; + if (to_delete > candidates) to_delete = candidates; + + qsort(sorted, (size_t)candidates, sizeof(*sorted), compare_age); for (n = 0; n < to_delete; n++) { lwsl_notice("%s: out of space (need %uMiB, free %uMiB): requesting removal of %s (age %llus)\n", @@ -563,6 +722,8 @@ saib_deletion_free_kib(unsigned int needed_kib) } } +done: + lwsac_free(&ctx.ac); return 0; } diff --git a/src/builder/b-private.h b/src/builder/b-private.h index 4a07c4b..5bcbafc 100644 --- a/src/builder/b-private.h +++ b/src/builder/b-private.h @@ -93,6 +93,20 @@ struct saib_opaque_spawn { #define SAI_STAY_POLL_US (20 * LWS_US_PER_SEC) #define SAI_CLEANUP_JOBS_INTERVAL_US (60ULL * 60ULL * LWS_US_PER_SEC) #define SAI_CLEANUP_JOB_DIR_MIN_AGE_SECS (24ull * 3600u) +/* + * The disk-pressure path may delete job dirs of any age, so it needs its own + * floor: a dir this young is either in use or between the steps of a task we + * are still building, and deleting it breaks that build. + */ +#define SAI_FREEKIB_JOB_DIR_MIN_AGE_SECS (30u * 60u) +/* + * How long a job dir hold survives without being renewed. A task whose steps + * are still coming renews its hold at every step, so this only has to outlast + * the gap between one step finishing and the next being offered (plus however + * long the server takes to get around to us). It is the backstop that stops a + * task that died on the server side pinning its job dir forever. + */ +#define SAI_JOBDIR_HOLD_MAX_SECS (2u * 3600u) struct saib_ws_pss; @@ -147,6 +161,7 @@ struct sai_builder { lws_dll2_owner_t sai_plat_server_owner; /* servers we connect to */ lws_dll2_owner_t devices_owner; /* sai_serial_t */ lws_dll2_owner_t lsp_owner; /* list of lws_spawn_piped */ + lws_dll2_owner_t jobdir_hold_owner; /* saib_jobdir_hold_t */ lws_dll2_owner_t shell_owner; /* list of sai_shell */ struct lws_ss_handle *ss_stay; @@ -412,7 +427,36 @@ saib_deletion_request(const char *job); extern void suspender_destroy(void); int -saib_deletion_free_kib(unsigned int needed_kib); +saib_deletion_free_kib(unsigned int needed_kib, const char *protect_vn); + +/* + * A task's build steps are each their own nspawn, so between steps there is no + * live nspawn pointing at the job dir and nothing else stops the deletion paths + * removing it. A hold on the job dir's 8-char "vn" name covers that gap. + */ + +typedef struct saib_jobdir_hold { + lws_dll2_t list; + char vn[16]; + uint64_t renewed; /* lws_now_secs() */ +} saib_jobdir_hold_t; + +void +saib_jobdir_hold(const char *vn); +void +saib_jobdir_release(const char *vn); +int +saib_jobdir_is_held(const char *vn); +void +saib_jobdir_holds_destroy(void); + +/* + * The job dir name for a task: the first 4 and last 4 chars of its uuid. Both + * the acceptance path (which must protect the dir before the nspawn exists) + * and the nspawn setup derive it, so it lives in one place. + */ +void +saib_task_jobdir_vn(char *dest, size_t dest_len, const char *task_uuid); int saib_reassess_idle_situation(void); void diff --git a/src/builder/b-sai.c b/src/builder/b-sai.c index 88320a8..7c1eb74 100644 --- a/src/builder/b-sai.c +++ b/src/builder/b-sai.c @@ -976,6 +976,8 @@ saib_app_run(int argc, const char **argv) lws_stub_destroy(&builder.mgr_deletion); #endif + saib_jobdir_holds_destroy(); + /* destroy the unique servers */ diff --git a/src/builder/b-task.c b/src/builder/b-task.c index ed89401..8b961dd 100644 --- a/src/builder/b-task.c +++ b/src/builder/b-task.c @@ -168,8 +168,53 @@ const char *git_helper_bat = +/* + * The job dir for a task is named from the first 4 and last 4 chars of its + * uuid. Both the acceptance path (which has to protect the dir before any + * nspawn exists) and the nspawn setup need it. + */ + +void +saib_task_jobdir_vn(char *dest, size_t dest_len, const char *task_uuid) +{ + size_t l = strlen(task_uuid); + + if (dest_len < 9 || l < 8) { + lws_strncpy(dest, task_uuid, dest_len); + return; + } + + memcpy(dest, task_uuid, 4); + memcpy(dest + 4, task_uuid + l - 4, 4); + dest[8] = '\0'; +} + +/* + * The server keeps re-offering a task we refused, so an explanation of the + * refusal has to be rate-limited or it becomes the task's log. One line per + * task per minute is enough to see why a task sat around. + */ + static int -saib_can_accept_task(sai_task_t *task, sai_plat_t *sp) +saib_refusal_loggable(const char *task_uuid) +{ + static char last_uuid[65]; + static lws_usec_t last_us; + lws_usec_t now = lws_now_usecs(); + + if (!strcmp(last_uuid, task_uuid) && + now - last_us < 60 * LWS_US_PER_SEC) + return 0; + + lws_strncpy(last_uuid, task_uuid, sizeof(last_uuid)); + last_us = now; + + return 1; +} + +static int +saib_can_accept_task(struct sai_plat_server *spm, sai_task_t *task, + sai_plat_t *sp) { unsigned int tc = sp->job_limit ? sp->job_limit : 6u; #if 0 @@ -214,22 +259,60 @@ saib_can_accept_task(sai_task_t *task, sai_plat_t *sp) } } - if ((((builder.ram_limit_kib * 4) / 3) - builder.ram_reserved_kib) < task->est_peak_mem_kib) { - lwsl_notice("%s: reject task %s: not enough RAM: task %u vs %u lim - %u res\n", __func__, - task->uuid, (unsigned int)task->est_peak_mem_kib, (unsigned int)builder.ram_limit_kib, (unsigned int)builder.ram_reserved_kib); - return 1; + { + uint64_t budget = (builder.ram_limit_kib * 4) / 3; + + budget = builder.ram_reserved_kib > budget ? 0 : + budget - builder.ram_reserved_kib; + + if (budget < task->est_peak_mem_kib) { + if (saib_refusal_loggable(task->uuid)) + saib_task_logf(spm, NULL, task->uuid, + "Builder %s can't take this step yet: " + "needs %uMiB RAM, %lluMiB of its " + "%lluMiB budget left", sp->name, + (unsigned int)(task->est_peak_mem_kib / 1024), + (unsigned long long)(budget / 1024), + (unsigned long long)((builder.ram_limit_kib * 4) / 3 / 1024)); + return 1; + } } { - unsigned int free_disk = saib_get_free_disk_kib(builder.home); - unsigned int needed_disk = task->est_disk_kib + (unsigned int)builder.disk_reserved_kib; + uint64_t free_disk = saib_get_free_disk_kib(builder.home); + uint64_t needed_disk = (uint64_t)task->est_disk_kib + + builder.disk_reserved_kib; + char vn[16]; /* leave 12.5% of free space as a safety margin */ if (free_disk < needed_disk + (free_disk / 8)) { - lwsl_notice("%s: reject task %s: not enough disk: needed %u (task %u + res %u), actual free %u\n", __func__, - task->uuid, needed_disk, (unsigned int)task->est_disk_kib, (unsigned int)builder.disk_reserved_kib, free_disk); - - saib_deletion_free_kib(needed_disk + (free_disk / 8)); + uint64_t want = needed_disk + (free_disk / 8); + + if (saib_refusal_loggable(task->uuid)) + saib_task_logf(spm, NULL, task->uuid, + "Builder %s can't take this step yet: " + "needs %lluMiB (step %uMiB + %lluMiB " + "reserved by other steps), only %lluMiB " + "free on %s", sp->name, + (unsigned long long)(needed_disk / 1024), + (unsigned int)(task->est_disk_kib / 1024), + (unsigned long long)(builder.disk_reserved_kib / 1024), + (unsigned long long)(free_disk / 1024), + builder.home); + + /* + * Try to make room, but never at the cost of the job + * dir of the very task we are being offered: its + * earlier steps' output is in there + */ + + saib_task_jobdir_vn(vn, sizeof(vn), task->uuid); + + if (want > 0xffffffffull) + want = 0xffffffffull; + + saib_deletion_free_kib((unsigned int)want, vn); + return 1; } } @@ -453,8 +536,20 @@ saib_task_destroy(struct sai_nspawn *ns) (unsigned int)ns->retcode, SAI_TASK_REASON_DESTROYED); - builder.ram_reserved_kib -= ns->task->est_peak_mem_kib; - builder.disk_reserved_kib -= ns->task->est_disk_kib; + /* + * Only ever give back what this ns took. Giving back the + * task's estimate for an ns that never reserved anything (it + * failed during setup) underflowed these, after which every + * offered task looked disk-starved and the deletion path + * purged job dirs to try to make room that was never missing. + */ + + builder.ram_reserved_kib -= builder.ram_reserved_kib < ns->res_ram_kib ? + builder.ram_reserved_kib : ns->res_ram_kib; + builder.disk_reserved_kib -= builder.disk_reserved_kib < ns->res_disk_kib ? + builder.disk_reserved_kib : ns->res_disk_kib; + ns->res_ram_kib = 0; + ns->res_disk_kib = 0; if (ns->spm) lws_sul_schedule(builder.context, 0, &ns->spm->sul_load_report, @@ -481,19 +576,45 @@ saib_task_destroy(struct sai_nspawn *ns) * since they will touch it during their close handling. */ - if (ns->task && (ns->retcode & SAISPRF_EXIT) && - (ns->retcode & 0xff) == 0 && - ns->task->build_step == ns->task->build_step_count - 1) { - /* Task succeeded completely, so clean up the directory. */ + if (ns->task && ns->inp_vn[0]) { + int step_ok = (ns->retcode & SAISPRF_EXIT) && + !(ns->retcode & 0xff); + int last_step = ns->task->build_step == + ns->task->build_step_count - 1; + + if (step_ok && !last_step) { + /* + * More steps of this task are coming, but each step is + * its own nspawn: from here until the next step is + * offered there is no live nspawn pointing at the job + * dir, and the deletion paths only spare dirs that + * have one. Hold it over the gap, or the next step + * finds its src/ tree gone. + */ + saib_jobdir_hold(ns->inp_vn); + } else { + /* + * Either we're done or we failed. Failed job dirs are + * deliberately left for inspection, so just drop the + * hold and let the normal age / disk pressure rules + * decide when they go. + */ + saib_jobdir_release(ns->inp_vn); + + if (step_ok) { + /* Task succeeded completely, clean up the dir */ - lwsl_notice("%s: task %s succeeded (all %d steps), requesting deletion of job dir %s\n", - __func__, ns->task->uuid, ns->task->build_step_count, ns->inp); + lwsl_notice("%s: task %s succeeded (all %d steps), requesting deletion of job dir %s\n", + __func__, ns->task->uuid, + ns->task->build_step_count, ns->inp); #if defined(LWS_WITH_STUB) - if (builder.mgr_deletion) { - if (saib_deletion_request(ns->inp_vn) < 0) - lwsl_err("%s: failed to queue deletion\n", __func__); - } + if (builder.mgr_deletion && + saib_deletion_request(ns->inp_vn) < 0) + lwsl_err("%s: failed to queue deletion\n", + __func__); #endif + } + } } if (ns->task && ns->task->ac_task_container) { @@ -968,7 +1089,7 @@ saib_consider_allocating_task(struct sai_plat_server *spm, lws_struct_args_t *a, * We're not already running it, let's consider accepting it */ - if (saib_can_accept_task(task, sp)) { + if (saib_can_accept_task(spm, task, sp)) { lwsl_warn("%s: builder rejects offered task\n", __func__); if (saib_queue_task_status_update(sp, spm, task->uuid, 0, SAI_TASK_REASON_BUSY)) { @@ -1153,9 +1274,13 @@ saib_consider_allocating_task(struct sai_plat_server *spm, lws_struct_args_t *a, goto ebail; } - memcpy(ns->inp_vn, ns->fsm.ovname, 4); - memcpy(ns->inp_vn + 4, ns->fsm.ovname + strlen(ns->fsm.ovname) - 4, 4); - ns->inp_vn[8] = '\0'; + saib_task_jobdir_vn(ns->inp_vn, sizeof(ns->inp_vn), ns->fsm.ovname); + + /* + * Renew the hold on the job dir for as long as this task has steps + * running or pending on us + */ + saib_jobdir_hold(ns->inp_vn); n += lws_snprintf(ns->inp + n, sizeof(ns->inp) - (unsigned int)n, "%s%c", ns->inp_vn, csep); @@ -1251,8 +1376,16 @@ saib_consider_allocating_task(struct sai_plat_server *spm, lws_struct_args_t *a, task->started = (uint64_t)lws_now_secs(); - builder.ram_reserved_kib += task->est_peak_mem_kib; - builder.disk_reserved_kib += task->est_disk_kib; + /* + * Remember what we reserved on the ns itself, so the destroy gives back + * exactly this and nothing at all for an ns that failed before getting + * here + */ + + ns->res_ram_kib = task->est_peak_mem_kib; + ns->res_disk_kib = task->est_disk_kib; + builder.ram_reserved_kib += ns->res_ram_kib; + builder.disk_reserved_kib += ns->res_disk_kib; if (saib_queue_task_status_update(sp, spm, task->uuid, 0, SAI_TASK_REASON_ACCEPTED)) goto bail; diff --git a/src/builder/b-ws-server.c b/src/builder/b-ws-server.c index 2b758c5..2ef2ba7 100644 --- a/src/builder/b-ws-server.c +++ b/src/builder/b-ws-server.c @@ -207,13 +207,28 @@ saib_m_rx(void *userobj, const uint8_t *in, size_t len, int flags) lwsl_notice("%s: received task cancel for %s, erase %d\n", __func__, can->task_uuid, can->erase); - if (can->erase) { + { + char vn[16]; + + /* + * Job dirs are named from the task uuid, they are not + * the uuid itself... asking for the uuid deleted just + * silently removed nothing. + */ + + saib_task_jobdir_vn(vn, sizeof(vn), can->task_uuid); + + /* it's not going to get any more steps */ + saib_jobdir_release(vn); + + if (can->erase) { #if defined(LWS_WITH_STUB) - if (builder.mgr_deletion) { - if (saib_deletion_request(can->task_uuid) < 0) - lwsl_err("%s: failed to queue deletion\n", __func__); - } + if (builder.mgr_deletion && + saib_deletion_request(vn) < 0) + lwsl_err("%s: failed to queue deletion\n", + __func__); #endif + } } lws_start_foreach_dll_safe(struct lws_dll2 *, mp, mp1, diff --git a/src/common/include/private.h b/src/common/include/private.h index 0211a91..b655921 100644 --- a/src/common/include/private.h +++ b/src/common/include/private.h @@ -337,6 +337,18 @@ struct sai_nspawn { int instance_ordinal; int count_artifacts; + /* + * What we actually added to builder.ram_reserved_kib / + * disk_reserved_kib for this nspawn, so the destroy can give back + * exactly that and no more. The task's estimates are not usable for + * that: an nspawn that failed before the reservation was made never + * added anything, and giving back its estimate anyway underflows the + * builder-wide counters (which then reject every subsequent task and + * make the deletion path purge job dirs). + */ + unsigned int res_ram_kib; + unsigned int res_disk_kib; + uint8_t spins; uint8_t state; /* NSSTATE_ */ uint8_t stdcount;
Page fetched 0s ago, creation time: 6ms (vhost etag hits: 0%, cache hits: 0%)