33 changed files with 41 additions and 9655 deletions
@ -1,179 +0,0 @@
|
||||
// etcp.h - Extended Transmission Control Protocol |
||||
#ifndef ETCP_H |
||||
#define ETCP_H |
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
#include "ll_queue.h" |
||||
|
||||
// Debug logging |
||||
#ifdef ETCP_DEBUG |
||||
#include <stdio.h> |
||||
#define ETCP_LOG(fmt, ...) printf("[ETCP] " fmt, ##__VA_ARGS__) |
||||
#else |
||||
#define ETCP_LOG(fmt, ...) ((void)0) |
||||
#endif |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
// Forward declarations |
||||
typedef struct epkt epkt_t; |
||||
|
||||
// Callback type for sending packets via UDP |
||||
typedef void (*etcp_tx_callback_t)(epkt_t* epkt, uint8_t* pkt, uint16_t len, void* arg); |
||||
|
||||
// Main ETCP structure |
||||
struct epkt { |
||||
// Queues |
||||
ll_queue_t* tx_queue; // Queue of data to send |
||||
ll_queue_t* output_queue; // Output queue (reassembled data) |
||||
|
||||
// Received packets sorted linked list |
||||
struct rx_packet* rx_list; |
||||
|
||||
// Sent packets (for retransmission) |
||||
struct sent_packet* sent_list; |
||||
|
||||
// Metrics |
||||
uint16_t rtt_last; // Last RTT (timebase 0.1us) |
||||
uint16_t rtt_avg_10; // Average RTT last 10 packets |
||||
uint16_t rtt_avg_100; // Average RTT last 100 packets |
||||
uint16_t jitter; // Jitter (averaged) |
||||
uint16_t bandwidth; // Current bandwidth (bytes per timebase) |
||||
uint32_t bytes_sent_total; // Total bytes sent |
||||
uint16_t last_sent_timestamp; // Timestamp of last sent packet |
||||
uint32_t bytes_allowed; // Calculated bytes allowed to send |
||||
|
||||
// State |
||||
uint16_t next_tx_id; // Next ID for transmission |
||||
uint16_t last_rx_id; // Last received ID (for ACK) |
||||
uint16_t last_delivered_id; // Last delivered to output_queue ID |
||||
|
||||
// Timers |
||||
void* next_tx_timer; // Timer for next transmission |
||||
void* retransmit_timer; // Timer for retransmissions |
||||
|
||||
// Callback |
||||
etcp_tx_callback_t tx_callback; |
||||
void* tx_callback_arg; |
||||
|
||||
// RTT history for averaging |
||||
uint16_t rtt_history[100]; |
||||
uint8_t rtt_history_idx; |
||||
uint8_t rtt_history_count; |
||||
|
||||
// Pending ACKs |
||||
uint16_t pending_ack_ids[32]; |
||||
uint16_t pending_ack_timestamps[32]; |
||||
uint8_t pending_ack_count; |
||||
|
||||
// Pending retransmission requests |
||||
uint16_t pending_retransmit_ids[32]; |
||||
uint8_t pending_retransmit_count; |
||||
|
||||
// Window management |
||||
uint32_t unacked_bytes; // Number of bytes sent but not yet acknowledged |
||||
uint32_t window_size; // Current window size in bytes (calculated) |
||||
uint16_t last_acked_id; // Last acknowledged packet ID |
||||
uint16_t last_rx_ack_id; // Latest received ACK ID from receiver |
||||
uint16_t retrans_timer_period; // Current retransmission timer period (timebase) |
||||
uint16_t next_retrans_time; // Time of next retransmission check |
||||
uint8_t window_blocked; // Flag: transmission blocked by window limit |
||||
}; |
||||
|
||||
// API Functions |
||||
|
||||
/** |
||||
* @brief Initialize new ETCP instance |
||||
* @return Pointer to new instance or NULL on error |
||||
*/ |
||||
epkt_t* etcp_init(void); |
||||
|
||||
/** |
||||
* @brief Free ETCP instance and all associated resources |
||||
* @param epkt Instance to free |
||||
*/ |
||||
void etcp_free(epkt_t* epkt); |
||||
|
||||
/** |
||||
* @brief Set callback for sending packets via UDP |
||||
* @param epkt ETCP instance |
||||
* @param cb Callback function |
||||
* @param arg User argument passed to callback |
||||
*/ |
||||
void etcp_set_callback(epkt_t* epkt, etcp_tx_callback_t cb, void* arg); |
||||
|
||||
/** |
||||
* @brief Process received UDP packet |
||||
* @param epkt ETCP instance |
||||
* @param pkt Packet data |
||||
* @param len Packet length |
||||
* @return 0 on success, -1 on error |
||||
*/ |
||||
int etcp_rx_input(epkt_t* epkt, uint8_t* pkt, uint16_t len); |
||||
|
||||
/** |
||||
* @brief Get total number of packets waiting in transmission queues |
||||
* @param epkt ETCP instance |
||||
* @return Number of packets |
||||
*/ |
||||
int etcp_tx_queue_size(epkt_t* epkt); |
||||
|
||||
/** |
||||
* @brief Put data into transmission queue |
||||
* @param epkt ETCP instance |
||||
* @param data Data to send |
||||
* @param len Data length |
||||
* @return 0 on success, -1 on error |
||||
*/ |
||||
int etcp_tx_put(epkt_t* epkt, uint8_t* data, uint16_t len); |
||||
|
||||
/** |
||||
* @brief Get output queue for reading received data |
||||
* @param epkt ETCP instance |
||||
* @return Pointer to output queue (ll_queue_t*) |
||||
*/ |
||||
ll_queue_t* etcp_get_output_queue(epkt_t* epkt); |
||||
|
||||
/** |
||||
* @brief Set bandwidth limit |
||||
* @param epkt ETCP instance |
||||
* @param bandwidth Bytes per timebase (0.1us) |
||||
*/ |
||||
void etcp_set_bandwidth(epkt_t* epkt, uint16_t bandwidth); |
||||
|
||||
/** |
||||
* @brief Update window size based on current RTT and bandwidth |
||||
* @param epkt ETCP instance |
||||
* Window size = RTT * bandwidth * 2 (bytes in flight) |
||||
*/ |
||||
void etcp_update_window(epkt_t* epkt); |
||||
|
||||
/** |
||||
* @brief Get current RTT |
||||
* @param epkt ETCP instance |
||||
* @return RTT in timebase units |
||||
*/ |
||||
uint16_t etcp_get_rtt(epkt_t* epkt); |
||||
|
||||
/** |
||||
* @brief Get current jitter |
||||
* @param epkt ETCP instance |
||||
* @return Jitter in timebase units |
||||
*/ |
||||
uint16_t etcp_get_jitter(epkt_t* epkt); |
||||
|
||||
/** |
||||
* @brief Reset connection state (clear queues, metrics, timers) |
||||
* @param epkt ETCP instance |
||||
* Note: Keeps bandwidth setting and callback |
||||
*/ |
||||
void etcp_reset(epkt_t* epkt); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
|
||||
#endif // ETCP_H |
||||
@ -1,33 +1,25 @@
|
||||
# Makefile for network emulator
|
||||
CC = gcc
|
||||
CFLAGS = -Os -std=c99 -Wall -Wextra -D_ISOC99_SOURCE
|
||||
INCLUDES = -I.
|
||||
INCLUDES = -I. -I../u_async
|
||||
|
||||
SRCS = net_emulator.c u_async.c timeout_heap.c
|
||||
SRCS = net_emulator.c
|
||||
OBJS = $(SRCS:.c=.o)
|
||||
UASYNC_OBJS = ../obj/src/u_async.o ../obj/src/timeout_heap.o
|
||||
|
||||
TARGET = net_emulator
|
||||
|
||||
all: $(TARGET) |
||||
|
||||
$(TARGET): $(OBJS) |
||||
$(TARGET): $(OBJS) $(UASYNC_OBJS) |
||||
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $^
|
||||
|
||||
test: test_timeout_heap test_uasync_random |
||||
./test_timeout_heap
|
||||
./test_uasync_random
|
||||
|
||||
test_timeout_heap: test_timeout_heap.o timeout_heap.o |
||||
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $^
|
||||
|
||||
test_uasync_random: test_uasync_random.o u_async.o timeout_heap.o |
||||
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $^
|
||||
|
||||
%.o: %.c |
||||
$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@
|
||||
|
||||
clean: |
||||
rm -f $(OBJS) $(TARGET) test_timeout_heap.o timeout_heap.o test_timeout_heap \
|
||||
test_uasync_random.o test_uasync_random
|
||||
rm -f $(OBJS) $(TARGET)
|
||||
|
||||
.PHONY: all clean test |
||||
.PHONY: all clean |
||||
@ -1,412 +0,0 @@
|
||||
#include "timeout_heap.h" |
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <assert.h> |
||||
#include <time.h> |
||||
#include <stdint.h> |
||||
|
||||
#define TEST_ASSERT(cond, msg) do { \ |
||||
if (!(cond)) { \
|
||||
fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, (msg)); \
|
||||
exit(1); \
|
||||
} \
|
||||
} while (0) |
||||
|
||||
// Element structure for large random test
|
||||
typedef struct { |
||||
uint64_t expiration; |
||||
void *data; |
||||
int cancelled; |
||||
} HeapElement; |
||||
|
||||
// Helper: compare two entries (for qsort)
|
||||
static int compare_entries(const void *a, const void *b) { |
||||
const TimeoutEntry *ea = (const TimeoutEntry *)a; |
||||
const TimeoutEntry *eb = (const TimeoutEntry *)b; |
||||
if (ea->expiration < eb->expiration) return -1; |
||||
if (ea->expiration > eb->expiration) return 1; |
||||
return 0; |
||||
} |
||||
|
||||
// Test 1: basic operations
|
||||
static void test_basic(void) { |
||||
printf("Test 1: basic operations...\n"); |
||||
TimeoutHeap *h = timeout_heap_create(10); |
||||
TEST_ASSERT(h != NULL, "heap creation"); |
||||
TEST_ASSERT(h->size == 0, "initial size zero"); |
||||
|
||||
// Push one element
|
||||
int data1 = 42; |
||||
int ret = timeout_heap_push(h, 100, &data1); |
||||
TEST_ASSERT(ret == 0, "push success"); |
||||
TEST_ASSERT(h->size == 1, "size after push"); |
||||
|
||||
// Peek
|
||||
TimeoutEntry entry; |
||||
ret = timeout_heap_peek(h, &entry); |
||||
TEST_ASSERT(ret == 0, "peek success"); |
||||
TEST_ASSERT(entry.expiration == 100, "peek expiration"); |
||||
TEST_ASSERT(entry.data == &data1, "peek data"); |
||||
TEST_ASSERT(entry.deleted == 0, "peek deleted flag"); |
||||
|
||||
// Pop
|
||||
ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == 0, "pop success"); |
||||
TEST_ASSERT(entry.expiration == 100, "pop expiration"); |
||||
TEST_ASSERT(entry.data == &data1, "pop data"); |
||||
TEST_ASSERT(h->size == 0, "size after pop"); |
||||
|
||||
// Pop empty
|
||||
ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == -1, "pop empty returns -1"); |
||||
|
||||
timeout_heap_destroy(h); |
||||
printf(" Passed\n"); |
||||
} |
||||
|
||||
// Test 2: ordering with random values
|
||||
static void test_ordering(size_t num_elements) { |
||||
printf("Test 2: ordering with %zu elements...\n", num_elements); |
||||
TimeoutHeap *h = timeout_heap_create(4); // small initial capacity
|
||||
TEST_ASSERT(h != NULL, "heap creation"); |
||||
|
||||
// Generate random expirations and store them
|
||||
TimeoutEntry *expected = malloc(num_elements * sizeof(TimeoutEntry)); |
||||
TEST_ASSERT(expected != NULL, "alloc expected array"); |
||||
|
||||
srand((unsigned int)time(NULL)); |
||||
for (size_t i = 0; i < num_elements; i++) { |
||||
// Generate unique expiration to avoid ties
|
||||
uint64_t exp = i * 1000000ULL + (rand() % 1000000); |
||||
int *data = malloc(sizeof(int)); |
||||
*data = (int)i; |
||||
int ret = timeout_heap_push(h, exp, data); |
||||
TEST_ASSERT(ret == 0, "push success"); |
||||
expected[i].expiration = exp; |
||||
expected[i].data = data; |
||||
expected[i].deleted = 0; |
||||
} |
||||
|
||||
// Sort expected by expiration
|
||||
qsort(expected, num_elements, sizeof(TimeoutEntry), compare_entries); |
||||
|
||||
// Pop and verify order
|
||||
for (size_t i = 0; i < num_elements; i++) { |
||||
TimeoutEntry entry; |
||||
int ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == 0, "pop success"); |
||||
TEST_ASSERT(entry.expiration == expected[i].expiration, "expiration order"); |
||||
TEST_ASSERT(entry.data == expected[i].data, "data matches"); |
||||
TEST_ASSERT(entry.deleted == 0, "not deleted"); |
||||
free(entry.data); // cleanup
|
||||
} |
||||
|
||||
// Heap should be empty
|
||||
TimeoutEntry entry; |
||||
int ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == -1, "heap empty after all pops"); |
||||
|
||||
free(expected); |
||||
timeout_heap_destroy(h); |
||||
printf(" Passed\n"); |
||||
} |
||||
|
||||
// Test 3: cancel operation
|
||||
static void test_cancel(void) { |
||||
printf("Test 3: cancel operation...\n"); |
||||
TimeoutHeap *h = timeout_heap_create(10); |
||||
TEST_ASSERT(h != NULL, "heap creation"); |
||||
|
||||
int data1 = 1, data2 = 2, data3 = 3; |
||||
timeout_heap_push(h, 100, &data1); |
||||
timeout_heap_push(h, 200, &data2); |
||||
timeout_heap_push(h, 300, &data3); |
||||
|
||||
// Cancel middle element
|
||||
int ret = timeout_heap_cancel(h, 200, &data2); |
||||
TEST_ASSERT(ret == 0, "cancel success"); |
||||
|
||||
// Pop and verify we get data1 then data3, not data2
|
||||
TimeoutEntry entry; |
||||
ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == 0, "pop 1 success"); |
||||
TEST_ASSERT(entry.expiration == 100, "first expiration"); |
||||
TEST_ASSERT(entry.data == &data1, "first data"); |
||||
|
||||
ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == 0, "pop 2 success"); |
||||
TEST_ASSERT(entry.expiration == 300, "second expiration"); |
||||
TEST_ASSERT(entry.data == &data3, "second data"); |
||||
|
||||
// Heap empty
|
||||
ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == -1, "heap empty"); |
||||
|
||||
timeout_heap_destroy(h); |
||||
printf(" Passed\n"); |
||||
} |
||||
|
||||
// Test 4: duplicate expiration times
|
||||
static void test_duplicate_expirations(void) { |
||||
printf("Test 4: duplicate expirations...\n"); |
||||
TimeoutHeap *h = timeout_heap_create(10); |
||||
TEST_ASSERT(h != NULL, "heap creation"); |
||||
|
||||
int data1 = 1, data2 = 2, data3 = 3; |
||||
// Push same expiration for data1 and data2
|
||||
timeout_heap_push(h, 100, &data1); |
||||
timeout_heap_push(h, 100, &data2); |
||||
timeout_heap_push(h, 200, &data3); |
||||
|
||||
// Pop should give both items with expiration 100 (order not guaranteed)
|
||||
TimeoutEntry entry; |
||||
int ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == 0, "first pop success"); |
||||
TEST_ASSERT(entry.expiration == 100, "first pop expiration"); |
||||
// Record which data pointer we got
|
||||
void *first_data = entry.data; |
||||
TEST_ASSERT(first_data == &data1 || first_data == &data2, "first pop data matches one of duplicates"); |
||||
|
||||
ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == 0, "second pop success"); |
||||
TEST_ASSERT(entry.expiration == 100, "second pop expiration"); |
||||
void *second_data = entry.data; |
||||
TEST_ASSERT(second_data == &data1 || second_data == &data2, "second pop data matches one of duplicates"); |
||||
TEST_ASSERT(first_data != second_data, "two different data pointers"); |
||||
|
||||
// Third pop should give expiration 200
|
||||
ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == 0, "third pop success"); |
||||
TEST_ASSERT(entry.expiration == 200, "third pop expiration"); |
||||
TEST_ASSERT(entry.data == &data3, "third pop data"); |
||||
|
||||
// Heap empty
|
||||
ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == -1, "heap empty"); |
||||
|
||||
timeout_heap_destroy(h); |
||||
printf(" Passed\n"); |
||||
} |
||||
|
||||
// Test 5: deleted flag handling (multiple deleted at root)
|
||||
static void test_deleted_root(void) { |
||||
printf("Test 5: deleted root handling...\n"); |
||||
TimeoutHeap *h = timeout_heap_create(10); |
||||
TEST_ASSERT(h != NULL, "heap creation"); |
||||
|
||||
int data1 = 1, data2 = 2, data3 = 3, data4 = 4; |
||||
timeout_heap_push(h, 100, &data1); |
||||
timeout_heap_push(h, 50, &data2); // earliest
|
||||
timeout_heap_push(h, 150, &data3); |
||||
timeout_heap_push(h, 75, &data4); |
||||
|
||||
// Cancel the earliest (root)
|
||||
timeout_heap_cancel(h, 50, &data2); |
||||
|
||||
// Peek should skip deleted root and give next earliest (75)
|
||||
TimeoutEntry entry; |
||||
int ret = timeout_heap_peek(h, &entry); |
||||
TEST_ASSERT(ret == 0, "peek success after root deleted"); |
||||
TEST_ASSERT(entry.expiration == 75, "peek expiration after skip"); |
||||
TEST_ASSERT(entry.data == &data4, "peek data after skip"); |
||||
|
||||
// Pop should also skip deleted root
|
||||
ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == 0, "pop success after root deleted"); |
||||
TEST_ASSERT(entry.expiration == 75, "pop expiration after skip"); |
||||
|
||||
// Next pop should be 100
|
||||
ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == 0, "pop second"); |
||||
TEST_ASSERT(entry.expiration == 100, "second pop expiration"); |
||||
|
||||
// Cancel 150, then push another earlier
|
||||
timeout_heap_cancel(h, 150, &data3); |
||||
int data5 = 5; |
||||
timeout_heap_push(h, 60, &data5); // earlier than 150 but after 100
|
||||
|
||||
// Pop should give 60
|
||||
ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == 0, "pop after new push"); |
||||
TEST_ASSERT(entry.expiration == 60, "expiration of newly pushed"); |
||||
|
||||
// Heap empty (150 deleted)
|
||||
ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == -1, "heap empty after deleted remaining"); |
||||
|
||||
timeout_heap_destroy(h); |
||||
printf(" Passed\n"); |
||||
} |
||||
|
||||
// Test 6: capacity growth
|
||||
static void test_growth(void) { |
||||
printf("Test 6: capacity growth...\n"); |
||||
TimeoutHeap *h = timeout_heap_create(2); // tiny capacity
|
||||
TEST_ASSERT(h != NULL, "heap creation"); |
||||
TEST_ASSERT(h->capacity == 2, "initial capacity"); |
||||
|
||||
// Push 10 elements
|
||||
for (int i = 0; i < 10; i++) { |
||||
int ret = timeout_heap_push(h, i * 10, NULL); |
||||
TEST_ASSERT(ret == 0, "push success"); |
||||
} |
||||
TEST_ASSERT(h->size == 10, "size after pushes"); |
||||
TEST_ASSERT(h->capacity >= 10, "capacity grown"); |
||||
|
||||
// Pop all, verify order
|
||||
uint64_t prev = 0; |
||||
for (int i = 0; i < 10; i++) { |
||||
TimeoutEntry entry; |
||||
int ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == 0, "pop success"); |
||||
TEST_ASSERT(entry.expiration == prev, "order after growth"); |
||||
prev = entry.expiration + 10; |
||||
} |
||||
|
||||
timeout_heap_destroy(h); |
||||
printf(" Passed\n"); |
||||
} |
||||
|
||||
// Comparison function for HeapElement
|
||||
static int compare_elements(const void *a, const void *b) { |
||||
const HeapElement *ea = (const HeapElement *)a; |
||||
const HeapElement *eb = (const HeapElement *)b; |
||||
if (ea->expiration < eb->expiration) return -1; |
||||
if (ea->expiration > eb->expiration) return 1; |
||||
return 0; |
||||
} |
||||
|
||||
// Test 8: large number of elements, random order, with some cancellations
|
||||
static void test_large_random(size_t num_elements, size_t cancel_percent) { |
||||
printf("Test 8: large random test (%zu elements, cancel %zu%%)...\n",
|
||||
num_elements, cancel_percent); |
||||
TimeoutHeap *h = timeout_heap_create(100); |
||||
TEST_ASSERT(h != NULL, "heap creation"); |
||||
|
||||
// Generate elements with unique expiration values
|
||||
HeapElement *elems = malloc(num_elements * sizeof(HeapElement)); |
||||
TEST_ASSERT(elems != NULL, "alloc elems"); |
||||
|
||||
// Create array of unique expiration values
|
||||
uint64_t *expirations = malloc(num_elements * sizeof(uint64_t)); |
||||
TEST_ASSERT(expirations != NULL, "alloc expirations"); |
||||
for (size_t i = 0; i < num_elements; i++) { |
||||
expirations[i] = i; // base unique value
|
||||
} |
||||
// Shuffle using Fisher-Yates
|
||||
srand((unsigned int)time(NULL) ^ 0x1234); |
||||
for (size_t i = num_elements - 1; i > 0; i--) { |
||||
size_t j = rand() % (i + 1); |
||||
uint64_t tmp = expirations[i]; |
||||
expirations[i] = expirations[j]; |
||||
expirations[j] = tmp; |
||||
} |
||||
|
||||
for (size_t i = 0; i < num_elements; i++) { |
||||
elems[i].expiration = expirations[i]; |
||||
elems[i].data = malloc(1); // unique pointer
|
||||
elems[i].cancelled = 0; |
||||
int ret = timeout_heap_push(h, elems[i].expiration, elems[i].data); |
||||
TEST_ASSERT(ret == 0, "push success"); |
||||
} |
||||
free(expirations); |
||||
|
||||
// Cancel some random elements
|
||||
size_t cancel_count = num_elements * cancel_percent / 100; |
||||
for (size_t i = 0; i < cancel_count; i++) { |
||||
size_t idx = rand() % num_elements; |
||||
if (!elems[idx].cancelled) { |
||||
int ret = timeout_heap_cancel(h, elems[idx].expiration, elems[idx].data); |
||||
TEST_ASSERT(ret == 0, "cancel success"); |
||||
elems[idx].cancelled = 1; |
||||
} |
||||
} |
||||
|
||||
// Build array of non-cancelled elements, sort by expiration
|
||||
HeapElement *remaining = malloc(num_elements * sizeof(HeapElement)); |
||||
TEST_ASSERT(remaining != NULL, "alloc remaining"); |
||||
size_t remain_count = 0; |
||||
for (size_t i = 0; i < num_elements; i++) { |
||||
if (!elems[i].cancelled) { |
||||
remaining[remain_count++] = elems[i]; |
||||
} |
||||
} |
||||
qsort(remaining, remain_count, sizeof(HeapElement), compare_elements); |
||||
|
||||
// Pop and verify order
|
||||
uint64_t last_expiration = 0; |
||||
for (size_t i = 0; i < remain_count; i++) { |
||||
TimeoutEntry entry; |
||||
int ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == 0, "pop success"); |
||||
TEST_ASSERT(entry.expiration == remaining[i].expiration, "expiration order"); |
||||
TEST_ASSERT(entry.data == remaining[i].data, "data matches"); |
||||
TEST_ASSERT(entry.expiration >= last_expiration, "non-decreasing"); |
||||
last_expiration = entry.expiration; |
||||
free(entry.data); |
||||
} |
||||
|
||||
// Heap empty
|
||||
TimeoutEntry entry; |
||||
int ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == -1, "heap empty after all pops"); |
||||
|
||||
// Free cancelled elements data
|
||||
for (size_t i = 0; i < num_elements; i++) { |
||||
if (elems[i].cancelled) { |
||||
free(elems[i].data); |
||||
} |
||||
} |
||||
|
||||
free(elems); |
||||
free(remaining); |
||||
timeout_heap_destroy(h); |
||||
printf(" Passed\n"); |
||||
} |
||||
|
||||
// Test 7: peek should not remove
|
||||
static void test_peek_no_remove(void) { |
||||
printf("Test 7: peek does not remove...\n"); |
||||
TimeoutHeap *h = timeout_heap_create(10); |
||||
TEST_ASSERT(h != NULL, "heap creation"); |
||||
|
||||
int data1 = 1, data2 = 2; |
||||
timeout_heap_push(h, 100, &data1); |
||||
timeout_heap_push(h, 200, &data2); |
||||
|
||||
TimeoutEntry entry; |
||||
int ret = timeout_heap_peek(h, &entry); |
||||
TEST_ASSERT(ret == 0, "peek success"); |
||||
TEST_ASSERT(entry.expiration == 100, "peek expiration"); |
||||
TEST_ASSERT(h->size == 2, "size unchanged after peek"); |
||||
|
||||
ret = timeout_heap_peek(h, &entry); |
||||
TEST_ASSERT(ret == 0, "second peek success"); |
||||
TEST_ASSERT(entry.expiration == 100, "peek same element"); |
||||
|
||||
// Pop should still give same element
|
||||
ret = timeout_heap_pop(h, &entry); |
||||
TEST_ASSERT(ret == 0, "pop after peek"); |
||||
TEST_ASSERT(entry.expiration == 100, "pop expiration"); |
||||
|
||||
timeout_heap_destroy(h); |
||||
printf(" Passed\n"); |
||||
} |
||||
|
||||
int main(void) { |
||||
printf("=== Timeout Heap Tests ===\n"); |
||||
|
||||
test_basic(); |
||||
test_ordering(100); // moderate number
|
||||
test_ordering(1000); // more elements
|
||||
test_cancel(); |
||||
test_duplicate_expirations(); |
||||
test_deleted_root(); |
||||
test_growth(); |
||||
test_peek_no_remove(); |
||||
test_large_random(5000, 10); // 5000 elements, 10% cancelled
|
||||
test_large_random(10000, 5); // 10000 elements, 5% cancelled
|
||||
|
||||
printf("\n=== All tests passed! ===\n"); |
||||
return 0; |
||||
} |
||||
@ -1,258 +0,0 @@
|
||||
// test_uasync_random.c - Test uasync instance with random timeouts
|
||||
#include "u_async.h" |
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <time.h> |
||||
|
||||
#define MAX_TIMEOUTS 1000 |
||||
#define MAX_TIMEOUT_TB 10000 // 1 second in timebase units (0.1 ms)
|
||||
#define CANCEL_PROBABILITY 20 // 20% chance to cancel a timeout
|
||||
#define NUM_ITERATIONS 50000 |
||||
|
||||
typedef struct { |
||||
int id; |
||||
int fired; |
||||
int cancelled; |
||||
uasync_t* ua; |
||||
} test_context_t; |
||||
|
||||
static int g_fired_count = 0; |
||||
static int g_cancelled_count = 0; |
||||
static int g_expected_fired = 0; |
||||
|
||||
static void timeout_callback(void* arg) { |
||||
test_context_t* ctx = (test_context_t*)arg; |
||||
if (!ctx) return; |
||||
|
||||
if (ctx->cancelled) { |
||||
printf("ERROR: Cancelled timeout %d fired!\n", ctx->id); |
||||
exit(1); |
||||
} |
||||
|
||||
ctx->fired = 1; |
||||
g_fired_count++; |
||||
|
||||
// Verify the timeout was removed from heap
|
||||
// (we can't directly check, but we trust the implementation)
|
||||
} |
||||
|
||||
static void run_random_test(void) { |
||||
printf("=== Random uasync timeout test ===\n"); |
||||
|
||||
// Create uasync instance
|
||||
uasync_t* ua = uasync_create(); |
||||
if (!ua) { |
||||
printf("FAIL: uasync_create returned NULL\n"); |
||||
exit(1); |
||||
} |
||||
|
||||
printf("Created uasync instance\n"); |
||||
|
||||
// Allocate test contexts
|
||||
test_context_t* contexts = malloc(MAX_TIMEOUTS * sizeof(test_context_t)); |
||||
if (!contexts) { |
||||
printf("FAIL: malloc failed\n"); |
||||
uasync_destroy(ua); |
||||
exit(1); |
||||
} |
||||
|
||||
memset(contexts, 0, MAX_TIMEOUTS * sizeof(test_context_t)); |
||||
|
||||
srand(time(NULL)); |
||||
|
||||
// Set random timeouts
|
||||
int active_timeouts = 0; |
||||
for (int i = 0; i < MAX_TIMEOUTS; i++) { |
||||
contexts[i].id = i; |
||||
contexts[i].ua = ua; |
||||
|
||||
// Random timeout between 1 and MAX_TIMEOUT_TB
|
||||
int timeout_tb = (rand() % MAX_TIMEOUT_TB) + 1; |
||||
|
||||
void* handle = uasync_set_timeout(ua, timeout_tb, &contexts[i], timeout_callback); |
||||
if (!handle) { |
||||
printf("WARN: uasync_set_timeout failed for timeout %d\n", i); |
||||
continue; |
||||
} |
||||
|
||||
// Store handle in context (we could store it, but we'll just track)
|
||||
contexts[i].fired = 0; |
||||
contexts[i].cancelled = 0; |
||||
active_timeouts++; |
||||
|
||||
// Randomly cancel some timeouts
|
||||
if (rand() % 100 < CANCEL_PROBABILITY) { |
||||
if (uasync_cancel_timeout(ua, handle) == ERR_OK) { |
||||
contexts[i].cancelled = 1; |
||||
g_cancelled_count++; |
||||
active_timeouts--; |
||||
} else { |
||||
printf("WARN: Failed to cancel timeout %d\n", i); |
||||
} |
||||
} |
||||
} |
||||
|
||||
g_expected_fired = active_timeouts; |
||||
printf("Set %d timeouts, cancelled %d, expecting %d to fire\n",
|
||||
MAX_TIMEOUTS, g_cancelled_count, g_expected_fired); |
||||
|
||||
// Run event loop for enough time to fire all timeouts
|
||||
// Calculate maximum timeout in milliseconds plus some margin
|
||||
int max_timeout_ms = (MAX_TIMEOUT_TB + 1000) / 10; // convert to ms with margin
|
||||
int iterations = 0; |
||||
|
||||
while (g_fired_count < g_expected_fired && iterations < NUM_ITERATIONS) { |
||||
// Poll with short timeout (10ms = 100 timebase units)
|
||||
uasync_poll(ua, 100); |
||||
iterations++; |
||||
|
||||
// Check if we've waited long enough
|
||||
if (iterations * 10 > max_timeout_ms * 5) { |
||||
// We've waited five times the max timeout, something might be wrong
|
||||
if (g_fired_count < g_expected_fired) { |
||||
printf("WARN: Only %d of %d timeouts fired after %d ms\n", |
||||
g_fired_count, g_expected_fired, iterations * 10); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Verify all non-cancelled timeouts fired
|
||||
int failed_fires = 0; |
||||
for (int i = 0; i < MAX_TIMEOUTS; i++) { |
||||
if (!contexts[i].cancelled && !contexts[i].fired) { |
||||
failed_fires++; |
||||
if (failed_fires < 10) { // Limit output
|
||||
printf("ERROR: Timeout %d didn't fire (not cancelled)\n", i); |
||||
} |
||||
} |
||||
|
||||
if (contexts[i].cancelled && contexts[i].fired) { |
||||
printf("ERROR: Cancelled timeout %d fired\n", i); |
||||
failed_fires++; |
||||
} |
||||
} |
||||
|
||||
if (failed_fires > 0) { |
||||
printf("FAIL: %d timeout failures\n", failed_fires); |
||||
} else { |
||||
printf("PASS: All %d expected timeouts fired correctly\n", g_fired_count); |
||||
} |
||||
|
||||
// Verify that no cancelled timeouts fired (already checked above)
|
||||
printf("Cancelled timeouts: %d (none should fire)\n", g_cancelled_count); |
||||
|
||||
// Cleanup
|
||||
free(contexts); |
||||
uasync_destroy(ua); |
||||
|
||||
if (failed_fires == 0) { |
||||
printf("=== Test PASSED ===\n"); |
||||
} else { |
||||
printf("=== Test FAILED ===\n"); |
||||
exit(1); |
||||
} |
||||
} |
||||
|
||||
static void simple_callback(void* arg) { |
||||
int* flag = (int*)arg; |
||||
*flag = 1; |
||||
} |
||||
|
||||
static void free_callback(void* arg) { |
||||
free(arg); |
||||
} |
||||
|
||||
static void test_instance_isolation(void) { |
||||
printf("\n=== Instance isolation test ===\n"); |
||||
|
||||
// Create two independent instances
|
||||
uasync_t* ua1 = uasync_create(); |
||||
uasync_t* ua2 = uasync_create(); |
||||
if (!ua1 || !ua2) { |
||||
printf("FAIL: Failed to create instances\n"); |
||||
exit(1); |
||||
} |
||||
|
||||
int fired1 = 0; |
||||
int fired2 = 0; |
||||
|
||||
void* timeout1 = uasync_set_timeout(ua1, 10, &fired1, simple_callback); |
||||
void* timeout2 = uasync_set_timeout(ua2, 20, &fired2, simple_callback); |
||||
|
||||
if (!timeout1 || !timeout2) { |
||||
printf("FAIL: Failed to set timeouts\n"); |
||||
exit(1); |
||||
} |
||||
|
||||
// Run first instance - should fire timeout1 only
|
||||
uasync_poll(ua1, 50); // 5ms wait
|
||||
if (fired1 != 1) { |
||||
printf("FAIL: Instance 1 timeout didn't fire\n"); |
||||
exit(1); |
||||
} |
||||
if (fired2 != 0) { |
||||
printf("FAIL: Instance 2 timeout fired incorrectly\n"); |
||||
exit(1); |
||||
} |
||||
|
||||
// Cancel timeout2
|
||||
if (uasync_cancel_timeout(ua2, timeout2) != ERR_OK) { |
||||
printf("FAIL: Failed to cancel timeout2\n"); |
||||
exit(1); |
||||
} |
||||
|
||||
// Run second instance - nothing should fire
|
||||
fired1 = 0; |
||||
uasync_poll(ua2, 50); |
||||
if (fired1 != 0) { |
||||
printf("FAIL: Instance 1 timeout fired from instance 2\n"); |
||||
exit(1); |
||||
} |
||||
if (fired2 != 0) { |
||||
printf("FAIL: Cancelled timeout fired\n"); |
||||
exit(1); |
||||
} |
||||
|
||||
uasync_destroy(ua1); |
||||
uasync_destroy(ua2); |
||||
|
||||
printf("PASS: Instance isolation works correctly\n"); |
||||
} |
||||
|
||||
static void test_memory_cleanup(void) { |
||||
printf("\n=== Memory cleanup test ===\n"); |
||||
|
||||
uasync_t* ua = uasync_create(); |
||||
if (!ua) { |
||||
printf("FAIL: uasync_create failed\n"); |
||||
exit(1); |
||||
} |
||||
|
||||
// Set many timeouts
|
||||
for (int i = 0; i < 100; i++) { |
||||
int* counter = malloc(sizeof(int)); |
||||
*counter = i; |
||||
uasync_set_timeout(ua, 1000 + i, counter, free_callback); |
||||
} |
||||
|
||||
// Destroy instance without waiting for timeouts
|
||||
// This should free all pending timeout nodes
|
||||
uasync_destroy(ua); |
||||
|
||||
printf("PASS: Memory cleanup completed\n"); |
||||
} |
||||
|
||||
int main(void) { |
||||
printf("Starting uasync random timeout tests\n"); |
||||
printf("====================================\n"); |
||||
|
||||
run_random_test(); |
||||
test_instance_isolation(); |
||||
test_memory_cleanup(); |
||||
|
||||
printf("\n====================================\n"); |
||||
printf("All tests PASSED!\n"); |
||||
return 0; |
||||
} |
||||
@ -1,134 +0,0 @@
|
||||
// timeout_heap.c
|
||||
|
||||
#include "timeout_heap.h" |
||||
#include <stdlib.h> |
||||
#include <stdio.h> // For potential error printing, optional |
||||
|
||||
// Helper macros for 1-based indices
|
||||
#define PARENT(i) ((i) / 2) |
||||
#define LEFT_CHILD(i) (2 * (i)) |
||||
#define RIGHT_CHILD(i) (2 * (i) + 1) |
||||
|
||||
TimeoutHeap *timeout_heap_create(size_t initial_capacity) { |
||||
TimeoutHeap *h = malloc(sizeof(TimeoutHeap)); |
||||
if (!h) return NULL; |
||||
h->heap = malloc(sizeof(TimeoutEntry) * initial_capacity); |
||||
if (!h->heap) { |
||||
free(h); |
||||
return NULL; |
||||
} |
||||
h->size = 0; |
||||
h->capacity = initial_capacity; |
||||
return h; |
||||
} |
||||
|
||||
void timeout_heap_destroy(TimeoutHeap *h) { |
||||
if (h) { |
||||
free(h->heap); |
||||
free(h); |
||||
} |
||||
} |
||||
|
||||
static void bubble_up(TimeoutHeap *h, size_t i) { |
||||
// i is 1-based
|
||||
while (i > 1 && h->heap[PARENT(i) - 1].expiration > h->heap[i - 1].expiration) { |
||||
// Swap with parent
|
||||
TimeoutEntry temp = h->heap[PARENT(i) - 1]; |
||||
h->heap[PARENT(i) - 1] = h->heap[i - 1]; |
||||
h->heap[i - 1] = temp; |
||||
i = PARENT(i); |
||||
} |
||||
} |
||||
|
||||
int timeout_heap_push(TimeoutHeap *h, TimeoutTime expiration, void *data) { |
||||
if (h->size == h->capacity) { |
||||
size_t new_cap = h->capacity ? h->capacity * 2 : 1; |
||||
TimeoutEntry *new_heap = realloc(h->heap, sizeof(TimeoutEntry) * new_cap); |
||||
if (!new_heap) return -1; // Allocation failed
|
||||
h->heap = new_heap; |
||||
h->capacity = new_cap; |
||||
} |
||||
|
||||
// Insert at end (0-based)
|
||||
size_t idx = h->size++; |
||||
h->heap[idx].expiration = expiration; |
||||
h->heap[idx].data = data; |
||||
h->heap[idx].deleted = 0; |
||||
|
||||
// Bubble up (1-based)
|
||||
bubble_up(h, idx + 1); |
||||
return 0; |
||||
} |
||||
|
||||
static void heapify_down(TimeoutHeap *h, size_t i) { |
||||
// i is 1-based
|
||||
while (1) { |
||||
size_t smallest = i; |
||||
size_t left = LEFT_CHILD(i); |
||||
size_t right = RIGHT_CHILD(i); |
||||
|
||||
if (left <= h->size && h->heap[left - 1].expiration < h->heap[smallest - 1].expiration) { |
||||
smallest = left; |
||||
} |
||||
if (right <= h->size && h->heap[right - 1].expiration < h->heap[smallest - 1].expiration) { |
||||
smallest = right; |
||||
} |
||||
if (smallest == i) break; |
||||
|
||||
// Swap
|
||||
TimeoutEntry temp = h->heap[smallest - 1]; |
||||
h->heap[smallest - 1] = h->heap[i - 1]; |
||||
h->heap[i - 1] = temp; |
||||
i = smallest; |
||||
} |
||||
} |
||||
|
||||
static void remove_root(TimeoutHeap *h) { |
||||
if (h->size == 0) return; |
||||
|
||||
// Move last to root
|
||||
h->heap[0] = h->heap[--h->size]; |
||||
|
||||
// Heapify down (1-based)
|
||||
if (h->size > 0) { |
||||
heapify_down(h, 1); |
||||
} |
||||
} |
||||
|
||||
int timeout_heap_peek(TimeoutHeap *h, TimeoutEntry *out) { |
||||
if (h->size == 0) return -1; |
||||
|
||||
// Skip deleted
|
||||
size_t i = 0; |
||||
while (i < h->size && h->heap[0].deleted) { |
||||
remove_root(h); |
||||
} |
||||
if (h->size == 0) return -1; |
||||
|
||||
*out = h->heap[0]; |
||||
return 0; |
||||
} |
||||
|
||||
int timeout_heap_pop(TimeoutHeap *h, TimeoutEntry *out) { |
||||
if (h->size == 0) return -1; |
||||
|
||||
// Skip deleted
|
||||
while (h->size > 0 && h->heap[0].deleted) { |
||||
remove_root(h); |
||||
} |
||||
if (h->size == 0) return -1; |
||||
|
||||
*out = h->heap[0]; |
||||
remove_root(h); |
||||
return 0; |
||||
} |
||||
|
||||
int timeout_heap_cancel(TimeoutHeap *h, TimeoutTime expiration, void *data) { |
||||
for (size_t i = 0; i < h->size; ++i) { |
||||
if (h->heap[i].expiration == expiration && h->heap[i].data == data) { |
||||
h->heap[i].deleted = 1; |
||||
return 0; |
||||
} |
||||
} |
||||
return -1; // Not found
|
||||
} |
||||
@ -1,72 +0,0 @@
|
||||
// timeout_heap.h
|
||||
|
||||
#ifndef TIMEOUT_HEAP_H |
||||
#define TIMEOUT_HEAP_H |
||||
|
||||
#include <stdint.h> // For uint64_t |
||||
#include <stddef.h> // For size_t |
||||
|
||||
typedef uint64_t TimeoutTime; // e.g., milliseconds since epoch or from now
|
||||
|
||||
typedef struct { |
||||
TimeoutTime expiration; // Sort key (smaller = earlier)
|
||||
void *data; // User data (e.g., callback or ID)
|
||||
int deleted; // 0 = active, 1 = deleted
|
||||
} TimeoutEntry; |
||||
|
||||
typedef struct { |
||||
TimeoutEntry *heap; // Dynamic array
|
||||
size_t size; // Current number of elements
|
||||
size_t capacity; // Allocated size
|
||||
} TimeoutHeap; |
||||
|
||||
/**
|
||||
* Create a new timeout heap with initial capacity. |
||||
* @param initial_capacity Starting capacity (will grow as needed). |
||||
* @return Pointer to the heap, or NULL on failure. |
||||
*/ |
||||
TimeoutHeap *timeout_heap_create(size_t initial_capacity); |
||||
|
||||
/**
|
||||
* Destroy the timeout heap and free resources. |
||||
* @param h The heap to destroy. |
||||
*/ |
||||
void timeout_heap_destroy(TimeoutHeap *h); |
||||
|
||||
/**
|
||||
* Insert a new timeout into the heap. |
||||
* @param h The heap. |
||||
* @param expiration The expiration time. |
||||
* @param data User data associated with the timeout. |
||||
* @return 0 on success, -1 on allocation failure. |
||||
*/ |
||||
int timeout_heap_push(TimeoutHeap *h, TimeoutTime expiration, void *data); |
||||
|
||||
/**
|
||||
* Peek at the earliest non-deleted timeout without removing it. |
||||
* @param h The heap. |
||||
* @param out Where to store the entry. |
||||
* @return 0 on success, -1 if empty. |
||||
*/ |
||||
int timeout_heap_peek(TimeoutHeap *h, TimeoutEntry *out); |
||||
|
||||
/**
|
||||
* Pop the earliest non-deleted timeout from the heap. |
||||
* @param h The heap. |
||||
* @param out Where to store the entry. |
||||
* @return 0 on success, -1 if empty. |
||||
*/ |
||||
int timeout_heap_pop(TimeoutHeap *h, TimeoutEntry *out); |
||||
|
||||
/**
|
||||
* Cancel a timeout by matching expiration and data. |
||||
* Scans the heap linearly, so O(n) time. |
||||
* Assumes combinations are unique; cancels the first match. |
||||
* @param h The heap. |
||||
* @param expiration The expiration time to match. |
||||
* @param data The data to match. |
||||
* @return 0 if found and canceled, -1 if not found. |
||||
*/ |
||||
int timeout_heap_cancel(TimeoutHeap *h, TimeoutTime expiration, void *data); |
||||
|
||||
#endif // TIMEOUT_HEAP_H
|
||||
@ -1,391 +0,0 @@
|
||||
// uasync.c
|
||||
|
||||
#include "u_async.h" |
||||
#include "timeout_heap.h" |
||||
#include <stdio.h> |
||||
#include <string.h> |
||||
#include <stdlib.h> |
||||
#include <unistd.h> |
||||
#include <errno.h> |
||||
|
||||
#ifndef FD_SETSIZE |
||||
#define FD_SETSIZE 1024 // Assume standard size; adjust if needed for your platform
|
||||
#endif |
||||
|
||||
// Timeout node
|
||||
struct timeout_node { |
||||
void* arg; |
||||
timeout_callback_t callback; |
||||
uint64_t expiration_ms; // absolute expiration time in milliseconds
|
||||
}; |
||||
|
||||
// Socket node
|
||||
struct socket_node { |
||||
int fd; |
||||
socket_callback_t read_cbk; |
||||
socket_callback_t write_cbk; |
||||
socket_callback_t except_cbk; |
||||
void* user_data; |
||||
struct socket_node* next; |
||||
}; |
||||
|
||||
// Uasync instance structure
|
||||
struct uasync_s { |
||||
TimeoutHeap* timeout_heap; // Heap for timeout management
|
||||
struct socket_node* socket_head; |
||||
int max_fd; |
||||
fd_set master_readfds; |
||||
fd_set master_writefds; |
||||
fd_set master_exceptfds; |
||||
struct socket_node* fd_to_node[FD_SETSIZE]; |
||||
}; |
||||
|
||||
|
||||
|
||||
// Helper to get current time
|
||||
static void get_current_time(struct timeval* tv) { |
||||
gettimeofday(tv, NULL); |
||||
} |
||||
|
||||
|
||||
|
||||
// Helper to add timeval: tv += dt (timebase units)
|
||||
static void timeval_add_tb(struct timeval* tv, int dt) { |
||||
tv->tv_usec += (dt % 10000) * 100; |
||||
tv->tv_sec += dt / 10000 + tv->tv_usec / 1000000; |
||||
tv->tv_usec %= 1000000; |
||||
} |
||||
|
||||
// Convert timeval to milliseconds (uint64_t)
|
||||
static uint64_t timeval_to_ms(const struct timeval* tv) { |
||||
return (uint64_t)tv->tv_sec * 1000ULL + (uint64_t)tv->tv_usec / 1000ULL; |
||||
} |
||||
|
||||
// Instance management
|
||||
uasync_t* uasync_create(void) { |
||||
uasync_t* ua = malloc(sizeof(uasync_t)); |
||||
if (!ua) return NULL; |
||||
|
||||
memset(ua, 0, sizeof(uasync_t)); |
||||
ua->max_fd = -1; |
||||
FD_ZERO(&ua->master_readfds); |
||||
FD_ZERO(&ua->master_writefds); |
||||
FD_ZERO(&ua->master_exceptfds); |
||||
memset(ua->fd_to_node, 0, sizeof(ua->fd_to_node)); |
||||
|
||||
ua->timeout_heap = timeout_heap_create(16); |
||||
if (!ua->timeout_heap) { |
||||
free(ua); |
||||
return NULL; |
||||
} |
||||
|
||||
return ua; |
||||
} |
||||
|
||||
void uasync_destroy(uasync_t* ua) { |
||||
if (!ua) return; |
||||
|
||||
// Free all remaining timeouts
|
||||
if (ua->timeout_heap) { |
||||
while (1) { |
||||
TimeoutEntry entry; |
||||
if (timeout_heap_pop(ua->timeout_heap, &entry) != 0) break; |
||||
struct timeout_node* node = (struct timeout_node*)entry.data; |
||||
free(node); |
||||
} |
||||
timeout_heap_destroy(ua->timeout_heap); |
||||
} |
||||
|
||||
// Free all socket nodes
|
||||
struct socket_node* cur = ua->socket_head; |
||||
while (cur) { |
||||
struct socket_node* next = cur->next; |
||||
free(cur); |
||||
cur = next; |
||||
} |
||||
|
||||
free(ua); |
||||
} |
||||
|
||||
void uasync_init(uasync_t* ua) { |
||||
if (!ua) return; |
||||
|
||||
ua->max_fd = -1; |
||||
FD_ZERO(&ua->master_readfds); |
||||
FD_ZERO(&ua->master_writefds); |
||||
FD_ZERO(&ua->master_exceptfds); |
||||
memset(ua->fd_to_node, 0, sizeof(ua->fd_to_node)); |
||||
|
||||
if (!ua->timeout_heap) { |
||||
ua->timeout_heap = timeout_heap_create(16); |
||||
} |
||||
} |
||||
|
||||
// Process expired timeouts
|
||||
static void process_timeouts(uasync_t* ua) { |
||||
if (!ua || !ua->timeout_heap) return; |
||||
|
||||
struct timeval now_tv; |
||||
get_current_time(&now_tv); |
||||
uint64_t now_ms = timeval_to_ms(&now_tv); |
||||
|
||||
int processed = 0; |
||||
while (1) { |
||||
TimeoutEntry entry; |
||||
if (timeout_heap_peek(ua->timeout_heap, &entry) != 0) break; |
||||
if (entry.expiration > now_ms) break; |
||||
|
||||
// DEBUG
|
||||
// printf("process_timeouts: expiration=%lu now=%lu diff=%ld heap_size=%zu\n",
|
||||
// entry.expiration, now_ms, (long)(now_ms - entry.expiration),
|
||||
// ua->timeout_heap->size);
|
||||
|
||||
// Pop the expired timeout
|
||||
timeout_heap_pop(ua->timeout_heap, &entry); |
||||
struct timeout_node* node = (struct timeout_node*)entry.data; |
||||
if (node && node->callback) { |
||||
node->callback(node->arg); |
||||
processed++; |
||||
} else { |
||||
printf("WARN: timeout node missing callback\n"); |
||||
} |
||||
free(node); |
||||
} |
||||
if (processed > 0) { |
||||
// printf("process_timeouts: processed %d timeouts, now_ms=%lu heap_size=%zu\n",
|
||||
// processed, now_ms, ua->timeout_heap->size);
|
||||
} |
||||
} |
||||
|
||||
// Compute time to next timeout
|
||||
static void get_next_timeout(uasync_t* ua, struct timeval* tv) { |
||||
if (!ua || !ua->timeout_heap) { |
||||
tv->tv_sec = 0; |
||||
tv->tv_usec = 0; |
||||
return; |
||||
} |
||||
|
||||
TimeoutEntry entry; |
||||
if (timeout_heap_peek(ua->timeout_heap, &entry) != 0) { |
||||
tv->tv_sec = 0; |
||||
tv->tv_usec = 0; |
||||
return; |
||||
} |
||||
|
||||
struct timeval now_tv; |
||||
get_current_time(&now_tv); |
||||
uint64_t now_ms = timeval_to_ms(&now_tv); |
||||
|
||||
if (entry.expiration <= now_ms) { |
||||
tv->tv_sec = 0; |
||||
tv->tv_usec = 0; |
||||
return; |
||||
} |
||||
|
||||
uint64_t delta_ms = entry.expiration - now_ms; |
||||
if (delta_ms > 86400000) { // Cap at 1 day to avoid overflow
|
||||
delta_ms = 86400000; |
||||
} |
||||
tv->tv_sec = delta_ms / 1000; |
||||
tv->tv_usec = (delta_ms % 1000) * 1000; |
||||
|
||||
// DEBUG
|
||||
// printf("get_next_timeout: expiration=%lu now=%lu delta=%lu sec=%ld usec=%ld\n",
|
||||
// entry.expiration, now_ms, delta_ms, (long)tv->tv_sec, (long)tv->tv_usec);
|
||||
} |
||||
|
||||
|
||||
|
||||
void* uasync_set_timeout(uasync_t* ua, int timeout_tb, void* arg, timeout_callback_t callback) { |
||||
// printf("SETTIMEOUT called\n");
|
||||
// fflush(stdout);
|
||||
if (!ua || timeout_tb < 0 || !callback) return NULL; |
||||
if (!ua->timeout_heap) return NULL; |
||||
|
||||
struct timeout_node* node = malloc(sizeof(struct timeout_node)); |
||||
if (!node) return NULL; |
||||
|
||||
node->arg = arg; |
||||
node->callback = callback; |
||||
|
||||
// Calculate expiration time in milliseconds
|
||||
struct timeval now; |
||||
get_current_time(&now); |
||||
uint64_t now_ms = timeval_to_ms(&now); |
||||
timeval_add_tb(&now, timeout_tb); |
||||
node->expiration_ms = timeval_to_ms(&now); |
||||
|
||||
// DEBUG
|
||||
// printf("set_timeout: tb=%d (%.2fms), now=%lu, expiration=%lu, delta=%ldms\n",
|
||||
// timeout_tb, timeout_tb/10.0, now_ms, node->expiration_ms,
|
||||
// (long)(node->expiration_ms - now_ms));
|
||||
|
||||
// Insert into heap
|
||||
if (timeout_heap_push(ua->timeout_heap, node->expiration_ms, node) != 0) { |
||||
free(node); |
||||
return NULL; |
||||
} |
||||
|
||||
return node; |
||||
} |
||||
|
||||
|
||||
|
||||
err_t uasync_cancel_timeout(uasync_t* ua, void* t_id) { |
||||
if (!ua || !t_id || !ua->timeout_heap) return ERR_FAIL; |
||||
|
||||
struct timeout_node* node = (struct timeout_node*)t_id; |
||||
|
||||
// Try to cancel from heap
|
||||
if (timeout_heap_cancel(ua->timeout_heap, node->expiration_ms, node) == 0) { |
||||
free(node); |
||||
return ERR_OK; |
||||
} |
||||
|
||||
// If not found in heap (maybe already expired and removed), still free
|
||||
free(node); |
||||
return ERR_FAIL; |
||||
} |
||||
|
||||
|
||||
|
||||
void* uasync_add_socket(uasync_t* ua, int fd, socket_callback_t read_cbk, socket_callback_t write_cbk, socket_callback_t except_cbk, void* user_data) { |
||||
if (!ua || fd < 0 || fd >= FD_SETSIZE) return NULL; // Add bounds check for map
|
||||
|
||||
struct socket_node* node = malloc(sizeof(struct socket_node)); |
||||
if (!node) return NULL; |
||||
|
||||
node->fd = fd; |
||||
node->read_cbk = read_cbk; |
||||
node->write_cbk = write_cbk; |
||||
node->except_cbk = except_cbk; |
||||
node->user_data = user_data; |
||||
node->next = ua->socket_head; |
||||
ua->socket_head = node; |
||||
|
||||
// Update masters (point 1)
|
||||
if (read_cbk) FD_SET(fd, &ua->master_readfds); |
||||
if (write_cbk) FD_SET(fd, &ua->master_writefds); |
||||
if (except_cbk) FD_SET(fd, &ua->master_exceptfds); |
||||
|
||||
// Update map (point 2)
|
||||
ua->fd_to_node[fd] = node; |
||||
|
||||
if (fd > ua->max_fd) ua->max_fd = fd; |
||||
|
||||
return node; |
||||
} |
||||
|
||||
|
||||
|
||||
err_t uasync_remove_socket(uasync_t* ua, void* s_id) { |
||||
if (!ua || !s_id) return ERR_FAIL; |
||||
|
||||
struct socket_node* node = (struct socket_node*)s_id; |
||||
struct socket_node* cur = ua->socket_head; |
||||
struct socket_node* prev = NULL; |
||||
|
||||
while (cur) { |
||||
if (cur == node) { |
||||
if (prev) { |
||||
prev->next = cur->next; |
||||
} else { |
||||
ua->socket_head = cur->next; |
||||
} |
||||
|
||||
// Update masters (point 1)
|
||||
if (node->read_cbk) FD_CLR(node->fd, &ua->master_readfds); |
||||
if (node->write_cbk) FD_CLR(node->fd, &ua->master_writefds); |
||||
if (node->except_cbk) FD_CLR(node->fd, &ua->master_exceptfds); |
||||
|
||||
// Update map (point 2)
|
||||
ua->fd_to_node[node->fd] = NULL; |
||||
|
||||
free(cur); |
||||
|
||||
// Update max_fd (simple rescan; optimize if needed by checking if removed == max_fd)
|
||||
ua->max_fd = -1; |
||||
cur = ua->socket_head; |
||||
while (cur) { |
||||
if (cur->fd > ua->max_fd) ua->max_fd = cur->fd; |
||||
cur = cur->next; |
||||
} |
||||
return ERR_OK; |
||||
} |
||||
prev = cur; |
||||
cur = cur->next; |
||||
} |
||||
return ERR_FAIL; |
||||
} |
||||
|
||||
|
||||
|
||||
void uasync_mainloop(uasync_t* ua) { |
||||
while (1) { |
||||
uasync_poll(ua, -1); /* infinite timeout */ |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
void uasync_poll(uasync_t* ua, int timeout_tb) { |
||||
if (!ua) return; |
||||
|
||||
/* Process expired timeouts */ |
||||
process_timeouts(ua); |
||||
|
||||
/* Prepare select with copies of masters */ |
||||
fd_set readfds = ua->master_readfds; |
||||
fd_set writefds = ua->master_writefds; |
||||
fd_set exceptfds = ua->master_exceptfds; |
||||
|
||||
struct timeval tv; |
||||
get_next_timeout(ua, &tv); |
||||
|
||||
/* If timeout_tb >= 0, compute timeout as min(timeout_tb, existing timer) */ |
||||
if (timeout_tb >= 0) { |
||||
struct timeval user_tv; |
||||
user_tv.tv_sec = timeout_tb / 10000; |
||||
user_tv.tv_usec = (timeout_tb % 10000) * 100; |
||||
|
||||
/* If no internal timer or user timeout is smaller */ |
||||
if (tv.tv_sec == 0 && tv.tv_usec == 0 && (!ua->timeout_heap || ua->timeout_heap->size == 0)) { |
||||
tv = user_tv; |
||||
} else if (user_tv.tv_sec < tv.tv_sec ||
|
||||
(user_tv.tv_sec == tv.tv_sec && user_tv.tv_usec < tv.tv_usec)) { |
||||
tv = user_tv; |
||||
} |
||||
} |
||||
|
||||
struct timeval* ptv = (tv.tv_sec == 0 && tv.tv_usec == 0 && (!ua->timeout_heap || ua->timeout_heap->size == 0)) ? NULL : &tv; |
||||
|
||||
int nfds = select(ua->max_fd + 1, &readfds, &writefds, &exceptfds, ptv); |
||||
if (nfds < 0) { |
||||
if (errno == EINTR) return; |
||||
perror("select"); |
||||
return; |
||||
} |
||||
|
||||
/* Process sockets with faster dispatch */ |
||||
for (int fd = 0; nfds > 0 && fd <= ua->max_fd; fd++) { |
||||
struct socket_node* node = ua->fd_to_node[fd]; |
||||
if (!node) continue; |
||||
|
||||
if (node->except_cbk && FD_ISSET(fd, &exceptfds)) { |
||||
node->except_cbk(fd, node->user_data); |
||||
nfds--; |
||||
} |
||||
if (node->read_cbk && FD_ISSET(fd, &readfds)) { |
||||
node->read_cbk(fd, node->user_data); |
||||
nfds--; |
||||
} |
||||
if (node->write_cbk && FD_ISSET(fd, &writefds)) { |
||||
node->write_cbk(fd, node->user_data); |
||||
nfds--; |
||||
} |
||||
} |
||||
|
||||
/* Process timeouts that may have expired during select. тайм-ауты после данных. */ |
||||
process_timeouts(ua); |
||||
|
||||
} |
||||
@ -1,45 +0,0 @@
|
||||
// uasync.h
|
||||
|
||||
// модуль асинхронных операций. добавляем сокеты и таймауты и mainloop их обслуживает.
|
||||
|
||||
#ifndef UASYNC_H |
||||
#define UASYNC_H |
||||
|
||||
#include <sys/time.h> |
||||
#include <sys/select.h> |
||||
|
||||
typedef void (*timeout_callback_t)(void* user_arg);// передаёт user_arg из uasync_set_timeout
|
||||
typedef void (*socket_callback_t)(int fd, void* user_arg);// передаёт user_arg из uasync_add_socket
|
||||
// user_arg полезен если нужно передать управляющую структуру. Ее можно выделить в памяти и в ней хранить всё что надо. т.е. при set_timeout передаём и получаем ее в callback-е
|
||||
|
||||
|
||||
// Error type
|
||||
typedef int err_t; |
||||
#define ERR_OK 0 |
||||
#define ERR_FAIL -1 |
||||
|
||||
// Opaque uasync instance handle
|
||||
typedef struct uasync_s uasync_t; |
||||
|
||||
// Instance management
|
||||
uasync_t* uasync_create(void); |
||||
void uasync_destroy(uasync_t* ua); |
||||
void uasync_init(uasync_t* ua); // Alternative: initialize existing instance
|
||||
|
||||
// Main loop
|
||||
void uasync_mainloop(uasync_t* ua);// бесконечный цикл, __noreturn
|
||||
|
||||
// Timeouts, timebase = 0.1 mS
|
||||
void* uasync_set_timeout(uasync_t* ua, int timeout_tb, void* user_arg, timeout_callback_t callback); |
||||
err_t uasync_cancel_timeout(uasync_t* ua, void* t_id); |
||||
|
||||
// Sockets
|
||||
void* uasync_add_socket(uasync_t* ua, int fd, socket_callback_t read_cbk, socket_callback_t write_cbk, socket_callback_t except_cbk, void* user_arg); |
||||
err_t uasync_remove_socket(uasync_t* ua, void* s_id); |
||||
|
||||
// Single iteration of event loop with timeout (timebase units)
|
||||
void uasync_poll(uasync_t* ua, int timeout_tb); |
||||
|
||||
|
||||
|
||||
#endif // UASYNC_H
|
||||
Loading…
Reference in new issue