You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

403 lines
22 KiB

/* Агент стенда: настоящие JOIN/Merkle/ETCP и автоматический выбор поставщиков.
* stdin/stdout — только JSONL RPC; диагностика ядра пишется в отдельный файл.
* Заголовок нагрузки и SHA256 проверяют opaque DATA после восстановления FEC. */
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <openssl/sha.h>
#include "../../lib/debug_config.h"
#include "../../lib/json_flat.h"
#include "../../lib/mem.h"
#include "../../src/chat/chat_core.h"
#include "../../src/chat/chat_sync.h"
#include "../../src/chat/invite_build.h"
#include "../../src/chat/invite_link.h"
#include "../../src/stream/stream_priv.h"
#include "../../src/stream/stream_member.h"
#include "../../src/utun_instance.h"
#define LAB_STREAMS 4
#define LAB_HISTOGRAM 4096
struct lab_stream {
struct STREAM_HANDLE *handle;
uint64_t source, generated, received, bytes, gaps, gap_batches, errors, ends, closed, next, first, latency_max;
uint64_t latency_sum, histogram[LAB_HISTOGRAM], last_data_ns, start_seq;
uint64_t seed, interval_ns, due_ns;
uint8_t *payload;
size_t size;
int publisher, started;
};
struct lab {
struct UTUN_INSTANCE *inst;
struct lab_stream streams[LAB_STREAMS];
struct chat_invite_build *invite;
uint64_t invite_id, group;
int errors;
};
static volatile sig_atomic_t stopping;
static uint64_t now_ns(void) {
struct timespec time;
clock_gettime(CLOCK_MONOTONIC, &time);
return (uint64_t)time.tv_sec * UINT64_C(1000000000) + time.tv_nsec;
}
static void stop_signal(int signal_number) { (void)signal_number; stopping = 1; }
static void json_string(const char *value) {
putchar('"');
for (const unsigned char *p = (const unsigned char *)value; *p; p++) {
if (*p == '"' || *p == '\\') { putchar('\\'); putchar(*p); }
else if (*p < 32) printf("\\u%04x", *p);
else putchar(*p);
}
putchar('"');
}
static void reply(uint64_t id, int result) {
if (result < 0) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "lab: RPC failed id=%" PRIu64 " rc=%d", id, result);
printf("{\"id\":%" PRIu64 ",\"rc\":%d}\n", id, result);
}
static int number(const char *json, const char *key, uint64_t *value) {
char buffer[32], *end;
if (json_flat_get(json, key, buffer, sizeof(buffer)) || !buffer[0] || buffer[0] == '-') return -1;
errno = 0;
uint64_t parsed = strtoull(buffer, &end, 10);
if (errno || *end) return -1;
*value = parsed;
return 0;
}
static struct lab_stream *slot(struct lab *lab, uint64_t source, int create) {
for (unsigned i = 0; i < LAB_STREAMS; i++) if (lab->streams[i].source == source) return &lab->streams[i];
for (unsigned i = 0; create && i < LAB_STREAMS; i++) {
if (!lab->streams[i].source) { lab->streams[i].source = source; return &lab->streams[i]; }
}
return NULL;
}
static void event(struct UTUN_INSTANCE *inst, const struct stream_event *event, void *arg) {
(void)inst;
struct lab *lab = arg;
struct lab_stream *stream = slot(lab, event->descriptor.source_node_id, 0);
if (!stream) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: event without owner source=%016" PRIx64, event->descriptor.source_node_id);
lab->errors++; return;
}
if (event->type == STREAM_EVENT_CLOSED) { stream->closed++; return; }
if (event->type == STREAM_EVENT_END) { stream->ends++; return; }
if (event->type == STREAM_EVENT_GAP) {
if (!stream->started) stream->start_seq = event->first_seq;
stream->gaps++; stream->gap_batches += event->last_seq - event->first_seq + 1;
if (stream->started && event->first_seq != stream->next) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: GAP cursor expected=%" PRIu64 " first=%" PRIu64, stream->next, event->first_seq);
stream->errors++;
}
stream->started = 1; stream->next = event->last_seq + 1;
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "lab: GAP source=%016" PRIx64 " first=%" PRIu64 " last=%" PRIu64,
stream->source, event->first_seq, event->last_seq);
return;
}
const uint8_t *data = event->data;
uint8_t digest[32];
int valid = event->length >= 80 && !memcmp(data, "SLABv001", 8);
if (valid) {
SHA256(data, event->length - 32, digest);
valid = !memcmp(digest, data + event->length - 32, 32) && stream_read_be(data + 8, 8) == event->first_seq &&
stream_read_be(data + 32, 8) == stream->source && stream_read_be(data + 40, 4) == event->length;
}
if (!valid || (stream->started && event->first_seq != stream->next)) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: DATA validation source=%016" PRIx64 " seq=%" PRIu64
" expected=%" PRIu64 " bytes=%zu checksum=%d", stream->source, event->first_seq, stream->next, event->length, valid);
stream->errors++;
}
if (!stream->started) stream->start_seq = event->first_seq;
if (!stream->received) stream->first = event->first_seq;
stream->started = 1; stream->next = event->last_seq + 1;
stream->received++; stream->bytes += event->length; stream->last_data_ns = now_ns();
if (valid) {
uint64_t generated_ns = stream_read_be(data + 16, 8);
if (generated_ns > stream->last_data_ns) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: CLOCK_MONOTONIC moved backwards"); stream->errors++; return;
}
uint64_t delay = (stream->last_data_ns - generated_ns) / UINT64_C(1000000);
stream->latency_sum += delay;
if (delay > stream->latency_max) stream->latency_max = delay;
stream->histogram[delay < LAB_HISTOGRAM ? delay : LAB_HISTOGRAM - 1]++;
}
}
static uint64_t percentile(const struct lab_stream *stream, unsigned percent) {
uint64_t target = (stream->received * percent + 99) / 100, count = 0;
if (!target) return 0;
for (unsigned i = 0; i < LAB_HISTOGRAM; i++) { count += stream->histogram[i]; if (count >= target) return i; }
return LAB_HISTOGRAM - 1;
}
static void statistics(struct lab *lab, uint64_t id, int loads) {
struct TOPO_GROUP *group = topo_groups_find(lab->inst->topo_groups, lab->group);
printf("{\"id\":%" PRIu64 ",\"rc\":0,\"node\":\"%" PRIu64 "\",\"group\":\"%" PRIu64
"\",\"errors\":%d,\"online\":[", id, lab->inst->node_id, lab->group, lab->errors);
int separator = 0;
if (stream_member_online(lab->inst, lab->group, lab->inst->node_id)) {
printf("\"%" PRIu64 "\"", lab->inst->node_id); separator = 1;
}
for (struct ll_entry *e = group ? group->nodes->head : NULL; e; e = e->next) {
struct TOPO_GROUP_NODE *node = (struct TOPO_GROUP_NODE *)e;
if (stream_member_online(lab->inst, lab->group, node->node_id)) {
printf("%s\"%" PRIu64 "\"", separator++ ? "," : "", node->node_id);
}
}
printf("],\"profile_status\":{"); separator = 0;
if (group) {
struct stream_member_state state;
printf("\"%" PRIu64 "\":%d", lab->inst->node_id,
stream_member_load(lab->inst, lab->group, lab->inst->node_id, &state, NULL)); separator = 1;
for (struct ll_entry *e = group->nodes->head; e; e = e->next) {
struct TOPO_GROUP_NODE *node = (struct TOPO_GROUP_NODE *)e;
printf("%s\"%" PRIu64 "\":%d", separator++ ? "," : "", node->node_id,
stream_member_load(lab->inst, lab->group, node->node_id, &state, NULL));
}
}
printf("},\"streams\":["); separator = 0;
for (unsigned i = 0; i < LAB_STREAMS; i++) {
struct lab_stream *stream = &lab->streams[i];
if (!stream->source) continue;
printf("%s{\"source\":\"%" PRIu64 "\",\"publisher\":%d,\"generated\":%" PRIu64 ",\"received\":%" PRIu64
",\"bytes\":%" PRIu64 ",\"gaps\":%" PRIu64 ",\"gap_batches\":%" PRIu64 ",\"errors\":%" PRIu64
",\"ends\":%" PRIu64 ",\"closed\":%" PRIu64 ",\"first_seq\":%" PRIu64 ",\"next_seq\":%" PRIu64
",\"start_seq\":%" PRIu64 ",\"age_ms\":%" PRIu64 ",\"latency_p50_ms\":%" PRIu64 ",\"latency_p95_ms\":%" PRIu64
",\"latency_p99_ms\":%" PRIu64 ",\"latency_max_ms\":%" PRIu64,
separator++ ? "," : "", stream->source, stream->publisher, stream->generated, stream->received,
stream->bytes, stream->gaps, stream->gap_batches, stream->errors, stream->ends, stream->closed,
stream->first, stream->next, stream->start_seq,
stream->last_data_ns ? (now_ns() - stream->last_data_ns) / UINT64_C(1000000) : 0,
percentile(stream, 50), percentile(stream, 95), percentile(stream, 99), stream->latency_max);
struct stream_info info;
if (stream->handle && stream_get_info(stream->handle, &info) == STREAM_OK) {
struct stream_selection_info selection;
memset(&selection, 0, sizeof(selection));
stream_selection_get_info(stream->handle, &selection);
printf(",\"active\":%d,\"epoch\":\"%" PRIu64 "\",\"stored_bytes\":%zu,\"capacity_bytes\":%zu"
",\"evicted_batches\":%" PRIu64 ",\"subflows\":%u,\"providers\":%u,\"probes\":%" PRIu64
",\"cycles\":%" PRIu64 ",\"migrations\":%" PRIu64 ",\"failures\":%" PRIu64 ",\"restarts\":%" PRIu64
",\"protected\":%d,\"paths\":[", stream_handle_active(stream->handle), info.descriptor.source_epoch,
info.buffer.stored_bytes, info.buffer.capacity_bytes, info.buffer.evicted_batches, selection.subflows,
selection.providers, selection.probes_started, selection.cycles, selection.migrations, selection.failures,
selection.restarts, selection.protected_by_fec);
struct stream_local local;
int path_separator = 0;
if (stream_local_get(lab->inst, &info.descriptor, &local) == STREAM_OK) {
for (uint8_t sub = 0; sub < info.descriptor.n; sub++) {
uint64_t first, last;
struct stream_path path;
if (stream_buffer_range(local.buffer, sub, &first, &last) != STREAM_OK ||
stream_buffer_path(local.buffer, last, sub, &path) != STREAM_OK) continue;
printf("%s{\"sub\":%u,\"seq\":%" PRIu64 ",\"nodes\":[", path_separator++ ? "," : "", sub, last);
for (unsigned p = 0; p < path.count; p++) printf("%s\"%" PRIu64 "\"", p ? "," : "", path.nodes[p]);
printf("]");
const uint8_t *wire; size_t length; uint64_t actual;
if (stream_buffer_read(local.buffer, last, sub, &wire, &length, &actual) == STREAM_OK)
printf(",\"source_timestamp_ms\":%u", (unsigned)stream_read_be(wire + STREAM_BLOCK_TIMESTAMP_OFF, 2));
struct stream_metric_snapshot metric;
if (stream_local_metrics(lab->inst, &info.descriptor, sub, get_time_tb() / 10, &metric) == STREAM_OK)
printf(",\"lag_ms\":%" PRIu64 ",\"source_rate\":%" PRIu64 ",\"receive_rate\":%" PRIu64
",\"late_blocks\":%" PRIu64 ",\"stops\":%" PRIu64 ",\"stopped\":%d",
metric.lag_ms, metric.source_rate_bytes_sec, metric.receive_rate_bytes_sec,
metric.late_blocks, metric.stops, metric.stopped);
printf("}");
}
}
printf("]");
}
printf("}");
}
printf("],\"peer_load\":["); separator = 0;
for (struct ll_entry *e = loads && group ? group->nodes->head : NULL; e; e = e->next) {
struct TOPO_GROUP_NODE *node = (struct TOPO_GROUP_NODE *)e;
struct stream_peer_load_info load;
if (stream_peer_get_load(lab->inst, node->node_id, &load) != STREAM_OK) continue;
printf("%s{\"peer\":\"%" PRIu64 "\",\"queued_bytes\":%" PRIu64 ",\"growth_rate\":%" PRIu64
",\"transport_bytes\":%" PRIu64 ",\"inflight_bytes\":%" PRIu64 ",\"pressure\":%d,\"confirmed\":%d}",
separator++ ? "," : "", node->node_id, load.load.queued.bytes, load.load.growth_rate,
load.transport_queued_bytes, load.inflight_bytes, load.load.pressure, load.load.confirmed);
}
printf("]}\n");
}
static void invite_ready(void *arg, int result, const char *link) {
struct lab *lab = arg;
lab->invite = NULL;
if (result) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "lab: invite failed id=%" PRIu64 " rc=%d", lab->invite_id, result);
printf("{\"id\":%" PRIu64 ",\"rc\":%d,\"link\":", lab->invite_id, result);
json_string(link ? link : ""); printf("}\n");
}
static void command(struct lab *lab, const char *json) {
uint64_t id = 0, source = 0, value = 0;
char operation[32];
if (number(json, "id", &id) || json_flat_get(json, "op", operation, sizeof(operation))) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: invalid RPC"); reply(id, -1); return;
}
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "lab: RPC id=%" PRIu64 " op=%s", id, operation);
if (!strcmp(operation, "stats")) { number(json, "loads", &value); statistics(lab, id, value != 0); return; }
if (!strcmp(operation, "quit")) { stopping = 1; reply(id, 0); return; }
if (!strcmp(operation, "create")) {
if (lab->group) { reply(id, -1); return; }
chat_core_create_channel_auto(lab->inst, "stream-netns");
for (struct ll_entry *e = lab->inst->topo_groups->group_list->head; e; e = e->next) {
struct TOPO_GROUP *group = (struct TOPO_GROUP *)e;
if (group->group_type == TOPO_GROUP_TYPE_CHAT) { lab->group = group->group_id; break; }
}
if (!lab->group) DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: CHAT group creation failed id=%" PRIu64, id);
printf("{\"id\":%" PRIu64 ",\"rc\":%d,\"group\":\"%" PRIu64 "\"}\n", id, lab->group ? 0 : -1, lab->group); return;
}
if (!strcmp(operation, "invite")) {
if (lab->invite || !lab->group) { reply(id, -1); return; }
lab->invite_id = id;
lab->invite = chat_invite_build_link(lab->inst, lab->group, lab->inst->node_id, NULL, invite_ready, lab);
if (!lab->invite) reply(id, -1);
return;
}
if (!strcmp(operation, "join")) {
char link[2048], error[256];
struct InviteData invite;
uint8_t addresses[2048];
if (json_flat_get(json, "link", link, sizeof(link)) || invite_link_decode(link, strlen(link), &invite, error, sizeof(error))) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: invalid invite"); reply(id, -1); return;
}
int size = invite_serialize_addrs(&invite, addresses, sizeof(addresses));
if (size <= 0) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: invite has no endpoint"); reply(id, -1); return; }
lab->group = invite.channelId;
chat_sync_connect_from_invite(lab->inst, invite.channelId, invite.nodeId, invite.pubkey, addresses,
invite.addrCount, size, invite.password_len ? invite.password : NULL, invite.join_key);
reply(id, 0); return;
}
int publish = !strcmp(operation, "publish");
if (publish) source = lab->inst->node_id;
else if (number(json, "source", &source)) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: missing source"); reply(id, -1); return; }
struct lab_stream *stream = slot(lab, source, publish || !strcmp(operation, "subscribe"));
if (!stream) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: no stream slot"); reply(id, -1); return; }
if (publish || !strcmp(operation, "subscribe")) {
if (stream->handle) { reply(id, 0); return; }
int result = publish ? stream_publish_open(lab->inst, lab->group, NULL, &stream->handle) :
stream_subscribe_open(lab->inst, lab->group, source, &stream->handle);
if (result == STREAM_OK && publish) {
struct stream_info info;
stream_get_info(stream->handle, &info);
stream->size = stream_input_size(&info.descriptor);
stream->payload = u_malloc(stream->size);
stream->publisher = 1;
if (!stream->payload) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: payload allocation failed"); stopping = 1; lab->errors++; }
if (!number(json, "seed", &value)) stream->seed = value;
}
reply(id, result); return;
}
if (!strcmp(operation, "rate") && stream->publisher && !number(json, "bps", &value)) {
stream->interval_ns = value ? stream->size * UINT64_C(8000000000) / value : 0;
if (value && !stream->interval_ns) stream->interval_ns = 1;
stream->due_ns = now_ns(); reply(id, 0); return;
}
if (!strcmp(operation, "finish") && stream->publisher) {
stream->interval_ns = 0; reply(id, stream_publish_finish(stream->handle)); return;
}
if (!strcmp(operation, "close")) {
stream->interval_ns = 0;
if (stream->handle) stream_close(stream->handle);
stream->handle = NULL; reply(id, 0); return;
}
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: unsupported RPC op=%s", operation); reply(id, -1);
}
static void generate(struct lab *lab) {
for (unsigned i = 0; i < LAB_STREAMS; i++) {
struct lab_stream *stream = &lab->streams[i];
for (unsigned burst = 0; stream->interval_ns && now_ns() >= stream->due_ns && burst < 8; burst++) {
uint8_t *data = stream->payload;
memset(data, 0, 48); memcpy(data, "SLABv001", 8);
stream_write_be(data + 8, stream->generated, 8); stream_write_be(data + 16, now_ns(), 8);
stream_write_be(data + 24, stream->seed, 8); stream_write_be(data + 32, stream->source, 8);
stream_write_be(data + 40, stream->size, 4);
uint64_t random = stream->seed ^ stream->generated ^ stream->source;
for (size_t p = 48; p < stream->size - 32; p++) {
random ^= random << 13; random ^= random >> 7; random ^= random << 17; data[p] = (uint8_t)random;
}
SHA256(data, stream->size - 32, data + stream->size - 32);
size_t accepted = 0;
int result = stream_publish_write(stream->handle, data, stream->size, &accepted);
if (result != STREAM_OK || accepted != stream->size) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: write failed rc=%d accepted=%zu expected=%zu", result, accepted, stream->size);
lab->errors++; stream->interval_ns = 0; break;
}
stream->generated++; stream->due_ns += stream->interval_ns;
}
}
}
int main(int argc, char **argv) {
if (argc == 3 && !strcmp(argv[1], "--identity")) {
uint8_t private_key[32], public_key[32];
SHA256((const unsigned char *)argv[2], strlen(argv[2]), private_key);
if (sc_compute_public_key_from_private(private_key, public_key) != SC_OK) return 1;
printf("{\"private\":\"");
for (unsigned i = 0; i < 32; i++) printf("%02x", private_key[i]);
printf("\",\"public\":\"");
for (unsigned i = 0; i < 32; i++) printf("%02x", public_key[i]);
printf("\",\"node\":\"%" PRIu64 "\"}\n", sc_derive_node_id_from_pubkey(public_key)); return 0;
}
if (argc != 3) { fprintf(stderr, "Usage: %s CONFIG LOG | --identity SEED\n", argv[0]); return 1; }
setvbuf(stdout, NULL, _IOLBF, 0);
signal(SIGTERM, stop_signal); signal(SIGINT, stop_signal); signal(SIGPIPE, SIG_IGN);
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_enable_console(0); debug_enable_file_output(argv[2], 1);
utun_instance_set_tun_init_enabled(0);
struct UASYNC *async = uasync_create();
struct lab *lab = u_calloc(1, sizeof(*lab));
if (!async || !lab) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: initialization allocation failed"); return 1; }
lab->inst = utun_instance_create(async, argv[1]);
if (!lab->inst || utun_core_start(lab->inst) || chat_service_start(lab->inst)) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: service startup failed"); return 1;
}
debug_enable_console(0);
stream_set_event_callback(lab->inst, event, lab);
if (fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: stdin nonblocking failed: %s", strerror(errno)); return 1;
}
printf("{\"ready\":true,\"node\":\"%" PRIu64 "\"}\n", lab->inst->node_id);
char input[8192]; size_t used = 0;
while (!stopping) {
uasync_poll(async, 100); generate(lab);
ssize_t size = read(STDIN_FILENO, input + used, sizeof(input) - used - 1);
if (size == 0) break;
if (size < 0) {
if (errno != EAGAIN && errno != EINTR) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: stdin failed: %s", strerror(errno)); lab->errors++; break; }
continue;
}
used += (size_t)size; input[used] = 0;
char *line;
while ((line = memchr(input, '\n', used))) {
*line = 0; command(lab, input);
size_t consumed = (size_t)(line - input) + 1;
memmove(input, input + consumed, used - consumed); used -= consumed; input[used] = 0;
}
if (used == sizeof(input) - 1) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: RPC exceeds input limit"); lab->errors++; break; }
}
if (lab->invite) chat_invite_build_cancel(lab->invite);
for (unsigned i = 0; i < LAB_STREAMS; i++) {
if (lab->streams[i].handle) stream_close(lab->streams[i].handle);
u_free(lab->streams[i].payload);
if (lab->streams[i].errors) lab->errors++;
}
int errors = lab->errors;
utun_instance_destroy(lab->inst); uasync_destroy(async, 1); u_free(lab);
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "lab: stopped errors=%d", errors);
debug_disable_file_output(); return errors ? 1 : 0;
}