feat: DPDK Market Data Feed Handler — production product app
Add complete market data gateway product: - app/market_feed.c: Zero-copy DPDK feed handler with latency histogram (min/avg/P50/P95/P99/max), per-symbol msg rate, periodic stats, signal handling, JSON parsing without library dependency. - demo/market_data_server.py: Zero-dependency simulated market data server (8 symbols, random walk pricing, subscribe/unsubscribe). - app/README.md: Full product documentation with architecture diagram. - Makefile: Add 'app' target for product build (build/market_feed). - src/main.c: Add -M market data streaming mode. Product is ready for: financial market data gateways, low-latency quote feeds, exchange connectivity testing. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+129
-2
@@ -35,6 +35,7 @@
|
||||
#include "ws_client.h"
|
||||
|
||||
#include <getopt.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ── Config ──────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -45,12 +46,16 @@ static struct
|
||||
char ws_path[256];
|
||||
char subscribe_msg[1024];
|
||||
int ping_interval; /* seconds, 0 = no ping */
|
||||
int market_data; /* market data streaming mode */
|
||||
int stat_interval; /* print stats every N messages */
|
||||
} g_cfg = {
|
||||
.server_ip = "192.168.1.100",
|
||||
.server_port = 8080,
|
||||
.ws_path = "/ws",
|
||||
.subscribe_msg = "",
|
||||
.ping_interval = 30,
|
||||
.market_data = 0,
|
||||
.stat_interval = 100,
|
||||
};
|
||||
|
||||
/* ── Command-line parsing ────────────────────────────────────────── */
|
||||
@@ -69,10 +74,16 @@ static void print_usage(const char *prog)
|
||||
printf(" -k, --keepalive <s> Ping interval in seconds (0=off, default: %d)\n",
|
||||
g_cfg.ping_interval);
|
||||
printf(" -l, --local-ip <ip> Local IP address\n");
|
||||
printf(" -M, --market-data Market data streaming mode (latency stats)\n");
|
||||
printf(" -h, --help Show this help\n");
|
||||
printf("\nExample:\n");
|
||||
printf("\nExamples:\n");
|
||||
printf(" # Simple echo:\n");
|
||||
printf(" %s -l 0-1 -n 2 -- -a 192.168.1.100 -p 8080 -s '{\"sub\":\"topic1\"}'\n",
|
||||
prog);
|
||||
printf(" # Market data:\n");
|
||||
printf(" %s -l 0-1 -n 2 -- -M -a 192.168.1.100 -p 8080 \\\n"
|
||||
" -s '{\"action\":\"subscribe\",\"symbols\":[\"AAPL\",\"GOOGL\"]}'\n",
|
||||
prog);
|
||||
}
|
||||
|
||||
static int parse_args(int argc, char *argv[])
|
||||
@@ -82,13 +93,14 @@ static int parse_args(int argc, char *argv[])
|
||||
{"port", required_argument, 0, 'p'},
|
||||
{"path", required_argument, 0, 'w'},
|
||||
{"subscribe", required_argument, 0, 's'},
|
||||
{"market-data", no_argument, 0, 'M'},
|
||||
{"keepalive", required_argument, 0, 'k'},
|
||||
{"local-ip", required_argument, 0, 'l'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{0, 0, 0, 0}};
|
||||
|
||||
int opt;
|
||||
while ((opt = getopt_long(argc, argv, "a:p:w:s:k:l:h",
|
||||
while ((opt = getopt_long(argc, argv, "a:p:w:s:k:l:Mh",
|
||||
long_opts, NULL)) != -1)
|
||||
{
|
||||
switch (opt)
|
||||
@@ -109,6 +121,9 @@ static int parse_args(int argc, char *argv[])
|
||||
case 'k':
|
||||
g_cfg.ping_interval = atoi(optarg);
|
||||
break;
|
||||
case 'M':
|
||||
g_cfg.market_data = 1;
|
||||
break;
|
||||
case 'l':
|
||||
{
|
||||
struct in_addr addr;
|
||||
@@ -161,6 +176,8 @@ int main(int argc, char *argv[])
|
||||
lip.s_addr = gLocalIp;
|
||||
printf("║ Local IP: %s\n", inet_ntoa(lip));
|
||||
printf("║ Ping: %ds\n", g_cfg.ping_interval);
|
||||
if (g_cfg.market_data)
|
||||
printf("║ Mode: Market Data (stats every %d msgs)\n", g_cfg.stat_interval);
|
||||
printf("╚══════════════════════════════════════════════╝\n\n");
|
||||
|
||||
/* ── mbuf pool ── */
|
||||
@@ -234,6 +251,116 @@ int main(int argc, char *argv[])
|
||||
ws_send_text(sockfd, g_cfg.subscribe_msg);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
* Market Data Mode: continuous streaming + latency stats
|
||||
* ══════════════════════════════════════════════════════════ */
|
||||
|
||||
if (g_cfg.market_data)
|
||||
{
|
||||
uint64_t tsc_hz = rte_get_timer_hz();
|
||||
int msg_count = 0;
|
||||
uint64_t lat_min_us = UINT64_MAX, lat_max_us = 0, lat_sum_us = 0;
|
||||
uint64_t last_stat = rte_get_timer_cycles();
|
||||
uint64_t stat_cyc = (uint64_t)g_cfg.stat_interval * tsc_hz;
|
||||
|
||||
printf("[MKT] Market data mode — tracking latency per quote\n");
|
||||
printf("[MKT] Stats every %d messages\n\n", g_cfg.stat_interval);
|
||||
|
||||
/* Read subscription ACK first */
|
||||
{
|
||||
uint8_t ack_buf[4096];
|
||||
int ack_op = 0, ack_len = sizeof(ack_buf);
|
||||
int r = ws_recv_frame(sockfd, &ack_op, ack_buf, &ack_len);
|
||||
if (r > 0) {
|
||||
ack_buf[ack_len] = '\0';
|
||||
printf("[MKT] Sub ACK: %s\n", ack_buf);
|
||||
}
|
||||
}
|
||||
|
||||
while (1)
|
||||
{
|
||||
/* ── DPDK RX ── */
|
||||
struct rte_mbuf *rx[BURST_SIZE];
|
||||
unsigned num_recvd = rte_eth_rx_burst(gDpdkPortId, 0, rx, BURST_SIZE);
|
||||
if (num_recvd > 0)
|
||||
rte_ring_sp_enqueue_burst(ring->in, (void **)rx, num_recvd, NULL);
|
||||
|
||||
/* ── DPDK TX ── */
|
||||
struct rte_mbuf *tx[BURST_SIZE];
|
||||
unsigned nb_tx = rte_ring_sc_dequeue_burst(ring->out, (void **)tx, BURST_SIZE, NULL);
|
||||
if (nb_tx > 0) {
|
||||
rte_eth_tx_burst(gDpdkPortId, 0, tx, nb_tx);
|
||||
for (unsigned i = 0; i < nb_tx; i++) rte_pktmbuf_free(tx[i]);
|
||||
}
|
||||
|
||||
/* ── WS receive ── */
|
||||
uint8_t ws_buf[WS_MAX_FRAME_SIZE];
|
||||
int ws_opcode = 0, ws_len = sizeof(ws_buf);
|
||||
int ws_ret = ws_recv_frame(sockfd, &ws_opcode, ws_buf, &ws_len);
|
||||
|
||||
if (ws_ret > 0 && ws_len > 0)
|
||||
{
|
||||
ws_buf[ws_len] = '\0';
|
||||
|
||||
if (ws_opcode == WS_OPCODE_TEXT)
|
||||
{
|
||||
/* Parse server timestamp for latency */
|
||||
uint64_t server_us = 0;
|
||||
char *ts = strstr((char *)ws_buf, "\"timestamp\":");
|
||||
if (ts) {
|
||||
server_us = strtoull(ts + 12, NULL, 10);
|
||||
uint64_t now_us = rte_get_timer_cycles() * 1000000 / tsc_hz;
|
||||
if (now_us > server_us) {
|
||||
uint64_t lat = now_us - server_us;
|
||||
if (lat < lat_min_us) lat_min_us = lat;
|
||||
if (lat > lat_max_us) lat_max_us = lat;
|
||||
lat_sum_us += lat;
|
||||
}
|
||||
}
|
||||
msg_count++;
|
||||
|
||||
/* Periodic stats */
|
||||
if (rte_get_timer_cycles() - last_stat >= stat_cyc)
|
||||
{
|
||||
double avg = msg_count > 0 ? (double)lat_sum_us / msg_count : 0;
|
||||
printf("[MKT STATS] msgs=%d | min=%luus avg=%.1fus max=%luus\n",
|
||||
msg_count, lat_min_us, avg, lat_max_us);
|
||||
|
||||
/* Also print the quote itself (first 80 chars) */
|
||||
char preview[81];
|
||||
strncpy(preview, (char *)ws_buf, 80);
|
||||
preview[80] = '\0';
|
||||
printf(" Latest: %s\n", preview);
|
||||
|
||||
/* Reset for next interval */
|
||||
msg_count = 0;
|
||||
lat_min_us = UINT64_MAX;
|
||||
lat_max_us = 0;
|
||||
lat_sum_us = 0;
|
||||
last_stat = rte_get_timer_cycles();
|
||||
}
|
||||
}
|
||||
else if (ws_opcode == WS_OPCODE_PONG)
|
||||
{
|
||||
/* Pong received — keepalive working */
|
||||
}
|
||||
}
|
||||
else if (ws_ret == 0)
|
||||
{
|
||||
printf("[MKT] Server closed connection\n");
|
||||
break;
|
||||
}
|
||||
else if (ws_ret < 0)
|
||||
{
|
||||
printf("[MKT] Recv error\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
nclose(sockfd);
|
||||
rte_eal_mp_wait_lcore();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
* Main loop: RX/TX + WS receive
|
||||
* ══════════════════════════════════════════════════════════ */
|
||||
|
||||
Reference in New Issue
Block a user