/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /** * @file ft_tt_tensix_claim.c * @brief Implementation of per-core Tensix claims (see ft_tt_tensix_claim.h for * the card-wide/per-core split and the reasoning behind every part of it). * * @status Off-hardware complete; this module never touches the card. * @issues None * @todo None */ #include "ft_tt_tensix_claim.h" #include #include #include #include "ft_tt_udrv.h" struct ft_tt_tensix_claim_set { ft_tt_udrv_claim_namespace ns; ft_tt_udrv_claim **claims; uint32_t *indices; size_t count; }; /** The reserved set is small by nature — a handful of families plus a serve. */ #define RESERVED_CAPACITY 128u static void refusal_reset(ft_tt_tensix_claim_refusal *refusal) { if (refusal) memset(refusal, 0, sizeof(*refusal)); } static void refusal_set(ft_tt_tensix_claim_refusal *refusal, ft_tt_tensix_claim_outcome outcome, uint32_t index, int detail) { if (!refusal) return; refusal->outcome = outcome; refusal->index = index; refusal->detail = detail; } /** * Name the live holder of `index` for a refusal message. * * The pid comes from the advisory record next to the lock, and the record can be * absent — a holder that died between taking the flock and writing the record * leaves none. That is not a failure: a refusal without a name is still a * refusal, and printing pid 0 is honest about not knowing. */ static void describe_holder(const ft_tt_udrv_claim_namespace *ns, uint32_t index, ft_tt_tensix_claim_refusal *refusal) { ft_tt_udrv_claim_info *info; if (!refusal) return; info = calloc((size_t)ns->count, sizeof(*info)); if (!info) return; if (ft_tt_udrv_claim_scan(ns, info, (size_t)ns->count, NULL) == FT_TT_UDRV_OK && index < ns->count) { refusal->pid = info[index].pid; memcpy(refusal->reason, info[index].reason, sizeof(refusal->reason)); refusal->reason[sizeof(refusal->reason) - 1u] = '\0'; } free(info); } static bool index_is_reserved(uint32_t index, const uint32_t *reserved, size_t reserved_count) { size_t slot; for (slot = 0u; slot < reserved_count; slot++) { if (reserved[slot] == index) return true; } return false; } static bool request_has_duplicate(const uint32_t *indices, size_t count, uint32_t *out_index) { size_t outer; for (outer = 0u; outer < count; outer++) { size_t inner; for (inner = outer + 1u; inner < count; inner++) { if (indices[outer] == indices[inner]) { *out_index = indices[outer]; return true; } } } return false; } /** Free every claim already taken, in reverse order, and the set itself. */ static void release_partial(ft_tt_tensix_claim_set *set, size_t held) { size_t slot = held; while (slot > 0u) { slot--; (void)ft_tt_udrv_claim_release(set->claims[slot]); } free(set->claims); free(set->indices); free(set); } const char *ft_tt_tensix_claim_resolve_card(const char *card) { const char *value; if (card && card[0] != '\0') return card; value = getenv(FT_TT_TENSIX_CLAIM_CARD_ENVIRONMENT); return (value && value[0] != '\0') ? value : FT_TT_TENSIX_CLAIM_CARD_DEFAULT; } const char *ft_tt_tensix_claim_resolve_root(const char *root) { const char *value; if (root && root[0] != '\0') return root; value = getenv(FT_TT_TENSIX_CLAIM_ROOT_ENVIRONMENT); /* NULL, not a literal: ft_tt_udrv_claim owns the default and must stay the * single place it is written down. */ return (value && value[0] != '\0') ? value : NULL; } int ft_tt_tensix_claim_parse_indices(const char *text, uint32_t *out, size_t capacity, size_t *out_count) { const char *cursor = text; size_t count = 0u; if (!out || !out_count || capacity == 0u) return FT_TT_UDRV_EINVAL; *out_count = 0u; if (!text || text[0] == '\0') return FT_TT_UDRV_OK; while (*cursor != '\0') { char *end = NULL; unsigned long first; unsigned long last; unsigned long value; /* strtoul SKIPS leading whitespace and accepts a sign, so "1, 2" and * "1,-2" would both parse without this — the first as a valid two-core * list and the second by wrapping through ULONG_MAX. Demanding a digit * is what makes a quoting mistake a refusal instead of a wrong core set. */ if (*cursor < '0' || *cursor > '9') return FT_TT_UDRV_EINVAL; first = strtoul(cursor, &end, 10); if (end == cursor || first >= FT_TT_TENSIX_CLAIM_INDEX_COUNT) return FT_TT_UDRV_EINVAL; cursor = end; last = first; if (*cursor == '-') { cursor++; if (*cursor < '0' || *cursor > '9') return FT_TT_UDRV_EINVAL; last = strtoul(cursor, &end, 10); if (end == cursor || last >= FT_TT_TENSIX_CLAIM_INDEX_COUNT || last < first) return FT_TT_UDRV_EINVAL; cursor = end; } for (value = first; value <= last; value++) { if (count >= capacity) return FT_TT_UDRV_EINVAL; out[count++] = (uint32_t)value; } if (*cursor == ',') cursor++; else if (*cursor != '\0') return FT_TT_UDRV_EINVAL; } *out_count = count; return FT_TT_UDRV_OK; } int ft_tt_tensix_claim_reserved_from_environment(uint32_t *out, size_t capacity, size_t *out_count) { return ft_tt_tensix_claim_parse_indices(getenv(FT_TT_TENSIX_CLAIM_RESERVED_ENVIRONMENT), out, capacity, out_count); } int ft_tt_tensix_claim_acquire(const ft_tt_tensix_claim_request *request, ft_tt_tensix_claim_set **out_set, ft_tt_tensix_claim_refusal *out_refusal) { uint32_t environment_reserved[RESERVED_CAPACITY]; const uint32_t *reserved; size_t reserved_count = 0u; ft_tt_tensix_claim_set *set = NULL; uint32_t duplicate = 0u; size_t slot; int rc; refusal_reset(out_refusal); if (!request || !out_set || !request->indices || request->count == 0u || request->count > FT_TT_TENSIX_CLAIM_MAX_CORES) { refusal_set(out_refusal, FT_TT_TENSIX_CLAIM_FAILED, 0u, FT_TT_UDRV_EINVAL); return FT_TT_UDRV_EINVAL; } /* A duplicated index would take the flock, then be refused by its own * holder's flock on the second attempt — a self-deadlock reported as * "held by pid ", which is the most confusing message available. */ if (request_has_duplicate(request->indices, request->count, &duplicate)) { refusal_set(out_refusal, FT_TT_TENSIX_CLAIM_DUPLICATE, duplicate, FT_TT_UDRV_EINVAL); return FT_TT_UDRV_EINVAL; } if (request->reserved) { reserved = request->reserved; reserved_count = request->reserved_count; } else { rc = ft_tt_tensix_claim_reserved_from_environment(environment_reserved, RESERVED_CAPACITY, &reserved_count); if (rc != FT_TT_UDRV_OK) { refusal_set(out_refusal, FT_TT_TENSIX_CLAIM_FAILED, 0u, rc); return rc; } reserved = environment_reserved; } /* Reserved is checked for the WHOLE request before anything is claimed, so a * request that names a reserved core never takes a lock it will hand back. */ for (slot = 0u; slot < request->count; slot++) { if (request->indices[slot] >= FT_TT_TENSIX_CLAIM_INDEX_COUNT) { refusal_set(out_refusal, FT_TT_TENSIX_CLAIM_FAILED, request->indices[slot], FT_TT_UDRV_EINVAL); return FT_TT_UDRV_EINVAL; } if (index_is_reserved(request->indices[slot], reserved, reserved_count)) { refusal_set(out_refusal, FT_TT_TENSIX_CLAIM_RESERVED, request->indices[slot], FT_TT_UDRV_EAGAIN); return FT_TT_UDRV_EAGAIN; } } set = calloc(1u, sizeof(*set)); if (!set) { refusal_set(out_refusal, FT_TT_TENSIX_CLAIM_FAILED, 0u, FT_TT_UDRV_ENOMEM); return FT_TT_UDRV_ENOMEM; } set->claims = calloc(request->count, sizeof(*set->claims)); set->indices = calloc(request->count, sizeof(*set->indices)); if (!set->claims || !set->indices) { release_partial(set, 0u); refusal_set(out_refusal, FT_TT_TENSIX_CLAIM_FAILED, 0u, FT_TT_UDRV_ENOMEM); return FT_TT_UDRV_ENOMEM; } rc = ft_tt_udrv_claim_namespace_init(&set->ns, ft_tt_tensix_claim_resolve_root(request->root), ft_tt_tensix_claim_resolve_card(request->card), FT_TT_TENSIX_CLAIM_NAMESPACE, FT_TT_TENSIX_CLAIM_INDEX_COUNT); if (rc != FT_TT_UDRV_OK) { release_partial(set, 0u); refusal_set(out_refusal, FT_TT_TENSIX_CLAIM_FAILED, 0u, rc); return rc; } for (slot = 0u; slot < request->count; slot++) { uint32_t index = request->indices[slot]; rc = ft_tt_udrv_claim_acquire(&set->ns, index, request->reason, &set->claims[slot]); if (rc != FT_TT_UDRV_OK) { ft_tt_tensix_claim_outcome outcome = (rc == FT_TT_UDRV_EAGAIN) ? FT_TT_TENSIX_CLAIM_HELD : FT_TT_TENSIX_CLAIM_FAILED; refusal_set(out_refusal, outcome, index, rc); if (outcome == FT_TT_TENSIX_CLAIM_HELD) describe_holder(&set->ns, index, out_refusal); /* Everything already taken goes back. A half-claimed set is a set * nobody can use and nobody else can claim. */ release_partial(set, slot); return rc; } set->indices[slot] = index; set->count = slot + 1u; } /* RECOVERY-ON-ACQUIRE, and only once the whole set is held: a retirement that * ran while a later core was still unclaimed could be retiring a core another * process is about to take. */ if (request->retire) { for (slot = 0u; slot < set->count; slot++) { int retired = request->retire(request->retire_context, set->indices[slot]); if (retired != 0) { refusal_set(out_refusal, FT_TT_TENSIX_CLAIM_UNRETIRED, set->indices[slot], retired); release_partial(set, set->count); return FT_TT_UDRV_EAGAIN; } } } *out_set = set; return FT_TT_UDRV_OK; } void ft_tt_tensix_claim_release(ft_tt_tensix_claim_set *set) { if (!set) return; release_partial(set, set->count); } size_t ft_tt_tensix_claim_count(const ft_tt_tensix_claim_set *set) { return set ? set->count : 0u; } uint32_t ft_tt_tensix_claim_index_at(const ft_tt_tensix_claim_set *set, size_t slot) { if (!set || slot >= set->count) return UINT32_MAX; return set->indices[slot]; } void ft_tt_tensix_claim_refusal_print(const ft_tt_tensix_claim_refusal *refusal) { if (!refusal || refusal->outcome == FT_TT_TENSIX_CLAIM_GRANTED) return; switch (refusal->outcome) { case FT_TT_TENSIX_CLAIM_HELD: if (refusal->pid > 0) { (void)fprintf(stderr, "refused: Tensix %u is held by pid %ld (\"%s\")\n", refusal->index, (long)refusal->pid, refusal->reason[0] != '\0' ? refusal->reason : "no reason recorded"); } else { (void)fprintf(stderr, "refused: Tensix %u is held by another process, which did not " "record its pid\n", refusal->index); } (void)fprintf(stderr, " Nothing was claimed. Wait for that process, or name different cores " "with %s / %s.\n", FT_TT_TENSIX_CLAIM_RESERVED_ENVIRONMENT, "FT_TT_TENSIX_INDICES"); break; case FT_TT_TENSIX_CLAIM_RESERVED: (void)fprintf(stderr, "refused: Tensix %u is RESERVED by %s — it belongs to another op family " "or to a live serve.\n Nothing was claimed.\n", refusal->index, FT_TT_TENSIX_CLAIM_RESERVED_ENVIRONMENT); break; case FT_TT_TENSIX_CLAIM_DUPLICATE: (void)fprintf(stderr, "refused: Tensix %u was named twice in one request. Nothing was " "claimed.\n", refusal->index); break; case FT_TT_TENSIX_CLAIM_UNRETIRED: (void)fprintf(stderr, "refused: Tensix %u was claimed but could not be retired (rc=%d), so it " "is not in a known state.\n" " A released claim is not a retired residency — the previous holder may " "have died with the core still running.\n" " The whole set was released. Check `ft_tt_core status %u --detail`.\n", refusal->index, refusal->detail, refusal->index); break; case FT_TT_TENSIX_CLAIM_FAILED: case FT_TT_TENSIX_CLAIM_GRANTED: default: (void)fprintf(stderr, "refused: could not claim Tensix %u (rc=%d). Nothing was claimed.\n", refusal->index, refusal->detail); break; } }