haproxy/src/stats-file.c
Aurelien DARRAGON b9ef55d56d MINOR: stats-file: use explicit unsigned integer bitshift for user slots
As reported in GH #3104, there remained a place where (1 << shift was
used to set or remove bits from uint64_t users bitfield. It is incorrect
and could lead to bugs for values > 32 bits.

Instead, let's use 1ULL to ensure the operation remains 64bits consistent.

No backport needed.
2025-09-08 13:38:49 +02:00

967 lines
27 KiB
C

#include <haproxy/stats-file.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <import/ebmbtree.h>
#include <import/ebsttree.h>
#include <import/ist.h>
#include <haproxy/api.h>
#include <haproxy/atomic.h>
#include <haproxy/buf.h>
#include <haproxy/chunk.h>
#include <haproxy/clock.h>
#include <haproxy/errors.h>
#include <haproxy/global.h>
#include <haproxy/guid.h>
#include <haproxy/intops.h>
#include <haproxy/list.h>
#include <haproxy/listener-t.h>
#include <haproxy/obj_type.h>
#include <haproxy/proxy-t.h>
#include <haproxy/server-t.h>
#include <haproxy/stats.h>
#include <haproxy/task.h>
#include <haproxy/time.h>
#include <haproxy/tools.h>
struct shm_stats_file_hdr *shm_stats_file_hdr = NULL;
static int shm_stats_file_fd = -1;
int shm_stats_file_slot = -1;
int shm_stats_file_max_objects = -1;
/* Dump all fields from <stats> into <out> for stats-file. */
int stats_dump_fields_file(struct buffer *out,
const struct field *line, size_t stats_count,
struct show_stat_ctx *ctx)
{
struct guid_node *guid;
struct listener *l;
int i;
switch (ctx->px_st) {
case STAT_PX_ST_FE:
case STAT_PX_ST_BE:
guid = &__objt_proxy(ctx->obj1)->guid;
break;
case STAT_PX_ST_LI:
l = LIST_ELEM(ctx->obj2, struct listener *, by_fe);
guid = &l->guid;
break;
case STAT_PX_ST_SV:
guid = &__objt_server(ctx->obj2)->guid;
break;
default:
ABORT_NOW();
return 1;
}
/* Skip objects without GUID. */
if (!guid->node.key)
return 1;
chunk_appendf(out, "%s,", (char *)guid->node.key);
for (i = 0; i < stats_count; ++i) {
/* Empty field for stats-file is used to skip its output,
* including any separator.
*/
if (field_format(line, i) == FF_EMPTY)
continue;
if (!stats_emit_raw_data_field(out, &line[i]))
return 0;
if (!chunk_strcat(out, ","))
return 0;
}
chunk_strcat(out, "\n");
return 1;
}
void stats_dump_file_header(int type, struct buffer *out)
{
const struct stat_col *col;
int i;
/* Caller must specified ither FE or BE. */
BUG_ON(!(type & ((1 << STATS_TYPE_FE) | (1 << STATS_TYPE_BE))));
if (type & (1 << STATS_TYPE_FE)) {
chunk_strcat(out, "#fe guid,");
for (i = 0; i < ST_I_PX_MAX; ++i) {
col = &stat_cols_px[i];
if (stcol_is_generic(col) &&
col->cap & (STATS_PX_CAP_FE|STATS_PX_CAP_LI)) {
chunk_appendf(out, "%s,", col->name);
}
}
}
else {
chunk_appendf(out, "#be guid,");
for (i = 0; i < ST_I_PX_MAX; ++i) {
col = &stat_cols_px[i];
if (stcol_is_generic(col) &&
col->cap & (STATS_PX_CAP_BE|STATS_PX_CAP_SRV)) {
chunk_appendf(out, "%s,", col->name);
}
}
}
chunk_strcat(out, "\n");
}
/* Parse an identified header line <header> starting with '#' character.
*
* If the section is recognized, <domain> will point to the current stats-file
* scope. <cols> will be filled as a matrix to identify each stat_col position
* using <st_tree> as prefilled proxy stats columns. If stats-file section is
* unknown, only <domain> will be set to STFILE_DOMAIN_UNSET.
*
* Returns 0 on success. On fatal error, non-zero is returned and parsing should
* be interrupted.
*/
static int parse_header_line(struct ist header, struct eb_root *st_tree,
enum stfile_domain *domain,
const struct stat_col *cols[])
{
enum stfile_domain dom = STFILE_DOMAIN_UNSET;
struct ist token;
char last;
int i;
header = iststrip(header);
last = istptr(header)[istlen(header) - 1];
token = istsplit(&header, ' ');
/* A header line is considered valid if:
* - a space delimiter is found and first token is several chars
* - last line character must be a comma separator
*/
if (!istlen(header) || istlen(token) == 1 || last != ',')
goto err;
if (isteq(token, ist("#fe")))
dom = STFILE_DOMAIN_PX_FE;
else if (isteq(token, ist("#be")))
dom = STFILE_DOMAIN_PX_BE;
/* Remove 'guid' field. */
token = istsplit(&header, ',');
if (!isteq(token, ist("guid"))) {
/* Fatal error if FE/BE domain without guid token. */
if (dom == STFILE_DOMAIN_PX_FE || dom == STFILE_DOMAIN_PX_BE)
goto err;
}
/* Unknown domain. Following lines should be ignored until next header. */
if (dom == STFILE_DOMAIN_UNSET)
return 0;
/* Generate matrix of stats column into cols[]. */
memset(cols, 0, sizeof(void *) * STAT_FILE_MAX_COL_COUNT);
i = 0;
while (istlen(header) && i < STAT_FILE_MAX_COL_COUNT) {
struct stcol_node *col_node;
const struct stat_col *col;
struct ebmb_node *node;
/* Lookup column by its name into <st_tree>. */
token = istsplit(&header, ',');
node = ebst_lookup(st_tree, ist0(token));
if (!node) {
++i;
continue;
}
col_node = ebmb_entry(node, struct stcol_node, name);
col = col_node->col;
/* Ignore column if its cap is not valid with current stats-file section. */
if ((dom == STFILE_DOMAIN_PX_FE &&
!(col->cap & (STATS_PX_CAP_FE|STATS_PX_CAP_LI))) ||
(dom == STFILE_DOMAIN_PX_BE &&
!(col->cap & (STATS_PX_CAP_BE|STATS_PX_CAP_SRV)))) {
++i;
continue;
}
cols[i] = col;
++i;
}
*domain = dom;
return 0;
err:
*domain = STFILE_DOMAIN_UNSET;
return 1;
}
/* Preload an individual counter instance stored at <counter> with <token>
* value> for the <col> stat column.
*
* Returns 0 on success else non-zero if counter was not updated.
*/
static int load_ctr(const struct stat_col *col, const struct ist token,
void* counter)
{
const enum field_nature fn = stcol_nature(col);
const enum field_format ff = stcol_format(col);
const char *ptr = istptr(token);
struct field value;
switch (ff) {
case FF_U64:
value.u.u64 = read_uint64(&ptr, istend(token));
break;
case FF_S32:
case FF_U32:
value.u.u32 = read_uint(&ptr, istend(token));
break;
default:
/* Unsupported field nature. */
return 1;
}
/* Do not load value if non numeric characters present. */
if (ptr != istend(token))
return 1;
if (fn == FN_COUNTER && ff == FF_U64) {
*(uint64_t *)counter = value.u.u64;
}
else if (fn == FN_RATE && ff == FF_U32) {
preload_freq_ctr(counter, value.u.u32);
}
else if (fn == FN_AGE && (ff == FF_U32 || ff == FF_S32)) {
*(uint32_t *)counter = ns_to_sec(now_ns) - value.u.u32;
}
else {
/* Unsupported field format/nature combination. */
return 1;
}
return 0;
}
/* Parse a non header stats-file line <line>. Specify current parsing <domain>
* and <cols> stats column matrix derived from the last header line.
*
* Returns 0 on success else non-zero.
*/
static int parse_stat_line(struct ist line,
enum stfile_domain domain,
const struct stat_col *cols[])
{
struct guid_node *node;
struct listener *li;
struct server *srv;
struct proxy *px;
struct ist token;
char *base_off, *base_off_shared;
char *guid;
int i, off;
token = istsplit(&line, ',');
guid = ist0(token);
if (!guid_is_valid_fmt(guid, NULL))
goto err;
node = guid_lookup(guid);
if (!node) {
/* Silently ignored unknown GUID. */
return 0;
}
switch (obj_type(node->obj_type)) {
case OBJ_TYPE_PROXY:
px = __objt_proxy(node->obj_type);
if (domain == STFILE_DOMAIN_PX_FE) {
if (!(px->cap & PR_CAP_FE))
return 0; /* silently ignored fe/be mismatch */
base_off_shared = (char *)px->fe_counters.shared.tg[0];
base_off = (char *)&px->fe_counters;
off = 0;
}
else if (domain == STFILE_DOMAIN_PX_BE) {
if (!(px->cap & PR_CAP_BE))
return 0; /* silently ignored fe/be mismatch */
base_off_shared = (char *)px->be_counters.shared.tg[0];
base_off = (char *)&px->be_counters;
off = 1;
}
else {
goto err;
}
break;
case OBJ_TYPE_LISTENER:
if (domain != STFILE_DOMAIN_PX_FE)
goto err;
li = __objt_listener(node->obj_type);
/* Listeners counters are not allocated if 'option socket-stats' unset. */
if (!li->counters)
return 0;
base_off_shared = (char *)li->counters->shared.tg[0];
base_off = (char *)li->counters;
off = 0;
break;
case OBJ_TYPE_SERVER:
if (domain != STFILE_DOMAIN_PX_BE)
goto err;
srv = __objt_server(node->obj_type);
base_off_shared = (char *)srv->counters.shared.tg[0];
base_off = (char *)&srv->counters;
off = 1;
break;
default:
goto err;
}
i = 0;
while (istlen(line) && i < STAT_FILE_MAX_COL_COUNT) {
const struct stat_col *col = cols[i++];
token = istsplit(&line, ',');
if (!istlen(token))
continue;
if (!col)
continue;
if (col->flags & STAT_COL_FL_SHARED)
load_ctr(col, token, base_off_shared + col->metric.offset[off]);
else
load_ctr(col, token, base_off + col->metric.offset[off]);
}
return 0;
err:
return 1;
}
/* Parse a stats-file and preload haproxy internal counters. */
void apply_stats_file(void)
{
const struct stat_col *cols[STAT_FILE_MAX_COL_COUNT];
struct eb_root st_tree = EB_ROOT;
enum stfile_domain domain;
int valid_format = 0;
FILE *file;
struct ist istline;
char *line = NULL;
int linenum;
if (!global.stats_file)
return;
file = fopen(global.stats_file, "r");
if (!file) {
ha_warning("config: Can't load stats-file '%s': cannot open file.\n", global.stats_file);
return;
}
/* Generate stat columns map indexed by name. */
if (generate_stat_tree(&st_tree, stat_cols_px)) {
ha_warning("config: Can't load stats-file '%s': not enough memory.\n", global.stats_file);
goto out;
}
line = malloc(sizeof(char) * LINESIZE);
if (!line) {
ha_warning("config: Can't load stats-file '%s': line alloc error.\n", global.stats_file);
goto out;
}
linenum = 0;
domain = STFILE_DOMAIN_UNSET;
while (1) {
if (!fgets(line, LINESIZE, file))
break;
++linenum;
istline = iststrip(ist(line));
if (!istlen(istline))
continue;
/* comment line starts by // */
if (istmatch(istline, ist("//")) != 0)
continue;
if (*istptr(istline) == '#') {
if (parse_header_line(istline, &st_tree, &domain, cols)) {
if (!valid_format) {
ha_warning("config: Invalid stats-file format in file '%s'.\n", global.stats_file);
break;
}
ha_warning("config: Ignored stats-file header line '%d' in file '%s'.\n", linenum, global.stats_file);
}
valid_format = 1;
}
else if (domain != STFILE_DOMAIN_UNSET) {
if (parse_stat_line(istline, domain, cols))
ha_warning("config: Ignored stats-file line %d in file '%s'.\n", linenum, global.stats_file);
}
else {
/* Stop parsing if first line is not a valid header.
* Allows to immediately stop reading garbage file.
*/
if (!valid_format) {
ha_warning("config: Invalid stats-file format in file '%s'.\n", global.stats_file);
break;
}
}
}
out:
while (!eb_is_empty(&st_tree)) {
struct ebmb_node *node = ebmb_first(&st_tree);
struct stcol_node *snode = ebmb_entry(node, struct stcol_node, name);
ebmb_delete(node);
ha_free(&snode);
}
ha_free(&line);
fclose(file);
}
/* returns 1 if <hdr> shm version is compatible with current version
* defined in stats-file-t.h or 0 if it is not compatible.
*/
static int shm_stats_file_check_ver(struct shm_stats_file_hdr *hdr)
{
/* for now we don't even support minor version difference but this may
* change later
*/
if (hdr->version.major != SHM_STATS_FILE_VER_MAJOR ||
hdr->version.minor != SHM_STATS_FILE_VER_MINOR)
return 0;
return 1;
}
static inline int shm_hb_is_stale(int hb)
{
return (hb == TICK_ETERNITY || tick_is_expired(hb, now_ms));
}
/* returns 1 if the slot <id> is free in <hdr>, else 0
*/
static int shm_stats_file_slot_isfree(struct shm_stats_file_hdr *hdr, int id)
{
int hb;
hb = HA_ATOMIC_LOAD(&hdr->slots[id].heartbeat);
return shm_hb_is_stale(hb);
}
/* returns free slot id on success or -1 if no more slots are available
* on success, the free slot is already reserved for the process pid
*/
int shm_stats_file_get_free_slot(struct shm_stats_file_hdr *hdr)
{
int it = 0;
int hb;
while (it < sizeof(hdr->slots) / sizeof(hdr->slots[0])) {
hb = HA_ATOMIC_LOAD(&hdr->slots[it].heartbeat);
/* try to own a stale entry */
while (shm_hb_is_stale(hb)) {
int new_hb = tick_add(now_ms, MS_TO_TICKS(SHM_STATS_FILE_HEARTBEAT_TIMEOUT * 1000));
if (HA_ATOMIC_CAS(&hdr->slots[it].heartbeat, &hb, new_hb)) {
shm_stats_file_hdr->slots[it].pid = getpid();
return it;
}
/* another process was faster than us */
__ha_cpu_relax();
}
it += 1;
}
return -1;
}
/* since shm file was opened using O_APPEND flag, let's grow
* the file by <bytes> in an atomic manner (O_APPEND offers such guarantee),
* so that even if multiple processes try to grow the file simultaneously,
* the file can only grow bigger and never shrink
*
* We do this way because ftruncate() between multiple processes
* could result in the file being shrunk if one of the process
* is not aware that the file was already expanded in the meantime
*
* Returns 1 on success and 0 on failure
*/
static int shm_file_grow(unsigned int bytes)
{
char buf[1024] = {0};
ssize_t ret;
while (bytes) {
ret = write(shm_stats_file_fd, buf, MIN(sizeof(buf), bytes));
if (ret <= 0)
return 0;
bytes -= ret;
}
return 1;
}
/* returns NULL if no free object or pointer to existing object if
* object can be reused
*/
static struct shm_stats_file_object *shm_stats_file_reuse_object(void)
{
int it = 0;
int objects;
struct shm_stats_file_object *free_obj;
BUG_ON(!shm_stats_file_hdr);
objects = HA_ATOMIC_LOAD(&shm_stats_file_hdr->objects);
if (!objects)
return NULL;
while (it < objects) {
uint64_t users;
int free = 0;
free_obj = SHM_STATS_FILE_OBJECT(shm_stats_file_hdr, it);
users = HA_ATOMIC_LOAD(&free_obj->users);
if (!users)
free = 1; // no doubt, no user using this object
else {
int slot = 0;
/* if one or multiple users crashed or forgot to remove their bit
* from obj->users but aren't making use of it anymore, we can detect
* it by checking if the process related to "used" users slot are still
* effectively active
*/
free = 1; // consider all users are inactive for now
while (slot < sizeof(shm_stats_file_hdr->slots) / sizeof(shm_stats_file_hdr->slots[0])) {
if ((users & (1ULL << slot)) &&
!shm_stats_file_slot_isfree(shm_stats_file_hdr, slot)) {
/* user still alive, so supposedly making use of it */
free = 0;
break;
}
slot++;
}
}
if (free) {
uint64_t nusers = (1ULL << shm_stats_file_slot);
/* we use CAS here because we want to make sure that we are the only
* process who exclusively owns the object as we are about to reset it.
* In case of failure, we also don't expect our bit to be set, so
* CAS is the best fit here. First we set the obj's users bits to 0
* to make sure no other process will try to preload it (it may hold
* garbage content) as we are about to reset it with our data, then
* we do another CAS to confirm we are the owner of the object
*/
if (HA_ATOMIC_CAS(&free_obj->users, &users, 0)) {
/* we set obj tgid to 0 so it can't be looked up in
* shm_stats_file_preload (tgid 0 is invalid)
*/
HA_ATOMIC_STORE(&free_obj->tgid, 0);
/* now we finally try to acquire the object */
users = 0;
if (HA_ATOMIC_CAS(&free_obj->users, &users, nusers))
return free_obj;
}
/* failed to CAS because of concurrent access, give up on this one */
}
it += 1;
}
return NULL;
}
/* returns pointer to new object in case of success and NULL in case
* of failure (if adding the maximum number of objects is already
* reached)
*
* <errmsg> will be set in case of failure to give more hints about the
* error, it must be freed accordingly
*/
struct shm_stats_file_object *shm_stats_file_add_object(char **errmsg)
{
struct shm_stats_file_object *new_obj;
uint64_t expected_users;
int objects, objects_slots;
static uint last_failed_attempt = TICK_ETERNITY;
/* if previous object reuse failed, don't try a new opportunistic
* reuse immediately because chances are high the new reuse attempt
* will also fail, and repeated failed reuse attempts could be costly
* with large number of objects
*/
if (last_failed_attempt != TICK_ETERNITY &&
!tick_is_expired(last_failed_attempt + MS_TO_TICKS(50), now_ms))
goto add;
new_obj = shm_stats_file_reuse_object();
if (new_obj) {
last_failed_attempt = TICK_ETERNITY;
return new_obj;
}
else
last_failed_attempt = now_ms;
add:
objects = HA_ATOMIC_LOAD(&shm_stats_file_hdr->objects);
if (objects >= shm_stats_file_max_objects) {
memprintf(errmsg, "Cannot add additionnal object to '%s' file, maximum number already reached (%d). "
"Adjust \"shm-stats-file-max-objects\" directive if needed.",
global.shm_stats_file, shm_stats_file_max_objects / global.nbtgroups);
return NULL;
}
objects_slots = HA_ATOMIC_LOAD(&shm_stats_file_hdr->objects_slots);
/* we increase objects slots by following half power of two curve to
* reduce waste while ensuring we don't grow the shm file (costly)
* too often
*/
if (objects + 1 > objects_slots) {
int nobjects_slots;
if (objects_slots < 2)
nobjects_slots = objects_slots + 1;
else if ((objects_slots & (objects_slots - 1)) == 0)
nobjects_slots = objects_slots + objects_slots / 2;
else
nobjects_slots = (objects_slots & (objects_slots - 1)) * 2;
if (shm_file_grow((nobjects_slots - objects_slots) * sizeof(struct shm_stats_file_object)) == 0) {
memprintf(errmsg, "Error when trying to increase shm stats file size for '%s': %s",
global.shm_stats_file, strerror(errno));
return NULL;
}
HA_ATOMIC_ADD(&shm_stats_file_hdr->objects_slots, nobjects_slots - objects_slots);
}
/* try to use this new slot */
new_obj = SHM_STATS_FILE_OBJECT(shm_stats_file_hdr, objects);
memset(new_obj, 0, sizeof(*new_obj)); // ensure object is reset before using it
if (HA_ATOMIC_FETCH_ADD(&shm_stats_file_hdr->objects, 1) != objects) {
/* a concurrent shm_stats_file_add_object stole our slot, retry */
__ha_cpu_relax();
goto add;
}
expected_users = 0;
if (!HA_ATOMIC_CAS(&new_obj->users, &expected_users, (1ULL << shm_stats_file_slot))) {
/* a parallel reuse stole us the object, retry */
__ha_cpu_relax();
goto add;
}
return new_obj;
};
static struct task *shm_stats_file_hb(struct task *task, void *context, unsigned int state)
{
if (stopping)
return NULL;
/* only update the heartbeat if it hasn't expired. Else it means the slot could have
* been reused and it isn't safe to use anymore.
* If this happens, raise a warning and stop using it
*/
if (tick_is_expired(HA_ATOMIC_LOAD(&shm_stats_file_hdr->slots[shm_stats_file_slot].heartbeat), now_ms)) {
ha_warning("shm_stats_file: heartbeat for the current process slot already expired, it is not safe to use it anymore\n");
task->expire = TICK_ETERNITY;
return task;
}
HA_ATOMIC_STORE(&shm_stats_file_hdr->slots[shm_stats_file_slot].heartbeat,
tick_add(now_ms, MS_TO_TICKS(SHM_STATS_FILE_HEARTBEAT_TIMEOUT * 1000)));
task->expire = tick_add(now_ms, 1000); // next update in 1 sec
return task;
}
/* loads shm_stats_file content and tries to associate existing objects from
* the shared memory (if any) to objects defined in current haproxy config
* based on GUIDs
*/
static void shm_stats_file_preload(void)
{
int it = 0;
int objects;
struct shm_stats_file_object *curr_obj;
BUG_ON(!shm_stats_file_hdr);
objects = HA_ATOMIC_LOAD(&shm_stats_file_hdr->objects);
if (!objects)
return; // nothing to do
while (it < objects) {
struct guid_node *node;
uint64_t users;
uint8_t obj_tgid;
curr_obj = SHM_STATS_FILE_OBJECT(shm_stats_file_hdr, it);
users = HA_ATOMIC_FETCH_OR(&curr_obj->users, (1ULL << shm_stats_file_slot));
/* ignore object if not used by anyone: when a process properly deinits,
* it removes its user bit from the object, thus an object without any
* bit should be considered as empty object
*/
if (!users)
goto release;
obj_tgid = HA_ATOMIC_LOAD(&curr_obj->tgid);
/* ignore object if greater than our max tgid */
if (obj_tgid <= global.nbtgroups &&
(node = guid_lookup(curr_obj->guid))) {
switch (*node->obj_type) {
case OBJ_TYPE_LISTENER:
{
struct listener *li;
BUG_ON(curr_obj->type != SHM_STATS_FILE_OBJECT_TYPE_FE);
li = __objt_listener(node->obj_type);
if (li->counters) // counters are optional for listeners
li->counters->shared.tg[obj_tgid - 1] = &curr_obj->data.fe;
break;
}
case OBJ_TYPE_SERVER:
{
struct server *sv;
BUG_ON(curr_obj->type != SHM_STATS_FILE_OBJECT_TYPE_BE);
sv = __objt_server(node->obj_type);
sv->counters.shared.tg[obj_tgid - 1] = &curr_obj->data.be;
break;
}
case OBJ_TYPE_PROXY:
{
struct proxy *px;
px = __objt_proxy(node->obj_type);
if (curr_obj->type == SHM_STATS_FILE_OBJECT_TYPE_FE)
px->fe_counters.shared.tg[obj_tgid - 1] = &curr_obj->data.fe;
else if (curr_obj->type == SHM_STATS_FILE_OBJECT_TYPE_BE)
px->be_counters.shared.tg[obj_tgid - 1] = &curr_obj->data.be;
else
goto release; // not supported
break;
}
default:
/* not supported */
goto release;
}
/* success */
goto next;
}
release:
/* we don't use this object, remove ourselves from object's users */
HA_ATOMIC_AND(&curr_obj->users, ~(1ULL << shm_stats_file_slot));
next:
it += 1;
}
}
/* prepare and and initialize shm stats memory file as needed */
int shm_stats_file_prepare(void)
{
struct task *heartbeat_task;
int first = 0; // process responsible for initializing the shm memory
int slot;
int objects;
BUG_ON(sizeof(struct shm_stats_file_hdr) != 672, "shm_stats_file_hdr struct size changed, "
"it is part of the exported API: ensure all precautions were taken (ie: shm_stats_file "
"version change) before adjusting this");
BUG_ON(sizeof(struct shm_stats_file_object) != 544, "shm_stats_file_object struct size changed, "
"is is part of the exported API: ensure all precautions were taken (ie: shm_stats_file "
"version change) before adjusting this");
/* do nothing if master process or shm_stats_file not configured */
if (master || !global.shm_stats_file)
return ERR_NONE;
/* compute final shm_stats_file_max_objects value */
if (shm_stats_file_max_objects == -1)
shm_stats_file_max_objects = SHM_STATS_FILE_MAX_OBJECTS * global.nbtgroups;
else
shm_stats_file_max_objects = shm_stats_file_max_objects * global.nbtgroups;
shm_stats_file_fd = open(global.shm_stats_file, O_RDWR | O_APPEND | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
if (shm_stats_file_fd == -1) {
shm_stats_file_fd = open(global.shm_stats_file, O_RDWR | O_APPEND, S_IRUSR | S_IWUSR);
if (shm_stats_file_fd == -1) {
ha_alert("config: cannot open shm stats file '%s': %s\n", global.shm_stats_file, strerror(errno));
return ERR_ALERT | ERR_FATAL;
}
}
else {
first = 1;
if (shm_file_grow(sizeof(*shm_stats_file_hdr)) == 0) {
ha_alert("config: unable to resize shm stats file '%s'\n", global.shm_stats_file);
return ERR_ALERT | ERR_FATAL;
}
}
/* mmap maximum contiguous address space for expected objects even if the backing shm is
* smaller: it will allow for on the fly shm resizing without having to remap
*/
shm_stats_file_hdr = mmap(NULL,
SHM_STATS_FILE_MAPPING_SIZE(shm_stats_file_max_objects),
PROT_READ | PROT_WRITE, MAP_SHARED, shm_stats_file_fd, 0);
if (shm_stats_file_hdr == MAP_FAILED || shm_stats_file_hdr == NULL) {
ha_alert("config: failed to map shm stats file '%s'\n", global.shm_stats_file);
return ERR_ALERT | ERR_FATAL;
}
if (first) {
/* let's init some members */
memset(shm_stats_file_hdr, 0, sizeof(*shm_stats_file_hdr));
shm_stats_file_hdr->version.major = SHM_STATS_FILE_VER_MAJOR;
shm_stats_file_hdr->version.minor = SHM_STATS_FILE_VER_MINOR;
/* set global clock for the first time */
shm_stats_file_hdr->global_now_ms = *global_now_ms;
shm_stats_file_hdr->global_now_ns = *global_now_ns;
shm_stats_file_hdr->now_offset = clock_get_now_offset();
}
else if (!shm_stats_file_check_ver(shm_stats_file_hdr))
goto err_version;
/* from now on use the shared global time */
global_now_ms = &shm_stats_file_hdr->global_now_ms;
global_now_ns = &shm_stats_file_hdr->global_now_ns;
if (!first) {
llong adjt_offset;
/* set adjusted offset which corresponds to the corrected offset
* relative to the initial offset stored in the shared memory instead
* of our process-local one
*/
adjt_offset = -clock_get_now_offset() + shm_stats_file_hdr->now_offset;
/* we now rely on global_now_* from the shm, so the boot
* offset that was initially applied in clock_init_process_date()
* is no longer relevant. So we fix it by applying the one from the
* initial process instead
*/
now_ns = now_ns + adjt_offset;
start_time_ns = start_time_ns + adjt_offset;
clock_set_now_offset(shm_stats_file_hdr->now_offset);
/* ensure global_now_* is consistent before continuing */
clock_update_global_date();
}
/* now that global_now_ns is accurate, recompute precise now_offset
* if needed (in case it is dynamic when monotonic clock not available)
*/
if (!th_ctx->curr_mono_time)
clock_set_now_offset(HA_ATOMIC_LOAD(global_now_ns) - tv_to_ns(&date));
/* sync local and global clocks, so all clocks are consistent */
clock_update_date(0, 1);
/* check if the map is outdated and must be reset:
* let's consider the map is outdated unless we find an occupied slot
*/
check_outdated:
if (first)
goto skip_check_outdated; // not needed
first = 1;
slot = 0;
objects = HA_ATOMIC_LOAD(&shm_stats_file_hdr->objects);
while (slot < sizeof(shm_stats_file_hdr->slots) / sizeof(shm_stats_file_hdr->slots[0])) {
if (!shm_stats_file_slot_isfree(shm_stats_file_hdr, slot)) {
first = 0;
break;
}
slot += 1;
}
if (first) {
/* no more slots occupied, let's reset the map but take some precautions
* to ensure another reset doesn't occur in parallel
*/
if (!HA_ATOMIC_CAS(&shm_stats_file_hdr->objects, &objects, 0)) {
__ha_cpu_relax();
goto check_outdated;
}
}
skip_check_outdated:
/* reserve our slot */
slot = shm_stats_file_get_free_slot(shm_stats_file_hdr);
if (slot == -1) {
ha_warning("config: failed to get shm stats file slot for '%s', all slots are occupied\n", global.shm_stats_file);
munmap(shm_stats_file_hdr, sizeof(*shm_stats_file_hdr));
return ERR_WARN;
}
shm_stats_file_slot = slot;
/* start the task responsible for updating the heartbeat */
heartbeat_task = task_new_anywhere();
if (!heartbeat_task) {
ha_alert("config: failed to create the heartbeat task for shm stats file '%s'\n", global.shm_stats_file);
return ERR_ALERT | ERR_FATAL;
}
heartbeat_task->process = shm_stats_file_hb;
task_schedule(heartbeat_task, tick_add(now_ms, 1000));
/* try to preload existing objects in the shm (if any) */
shm_stats_file_preload();
end:
return ERR_NONE;
err_version:
ha_warning("config: incompatible map shm stats file version '%s'\n", global.shm_stats_file);
return ERR_WARN;
}
static void cleanup_shm_stats_file(void)
{
if (shm_stats_file_hdr) {
/* mark the process slot we occupied as unused */
HA_ATOMIC_STORE(&shm_stats_file_hdr->slots[shm_stats_file_slot].heartbeat, TICK_ETERNITY);
shm_stats_file_hdr->slots[shm_stats_file_slot].pid = -1;
munmap(shm_stats_file_hdr, SHM_STATS_FILE_MAPPING_SIZE(shm_stats_file_max_objects));
close(shm_stats_file_fd);
}
}
REGISTER_POST_DEINIT(cleanup_shm_stats_file);