Foundry/tt/ft_tt_tensix_claim.c

398 lines
14 KiB
C

/* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 <self>", 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;
}
}