Skip to content

Commit 2ccc021

Browse files
authored
Merge pull request #9 from dagrigorev/develop
Develop
2 parents fa59cd3 + 5796f85 commit 2ccc021

9 files changed

Lines changed: 277 additions & 25 deletions

File tree

.env.secret.example

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
# .env.secret — sensitive values for docker-compose.k8s.yml
1+
# .env.secret — sensitive values for docker-compose
22
#
33
# !! DO NOT COMMIT THIS FILE TO GIT !!
44
# Add to .gitignore: echo ".env.secret" >> .gitignore
55
#
66
# Copy this file to .env.secret and fill in real values.
7+
# Passwords below match the hashes in docker/config/vcache.acl.
78

8-
# VoidCache password (leave blank to disable auth)
9-
VC_PASSWORD=
9+
# VoidCache passwords (must match hashes in vcache.acl)
10+
VC_PASSWORD_ADMIN=adminpass
11+
VC_PASSWORD_APP=apppass
12+
VC_PASSWORD_READER=readonly
13+
VC_PASSWORD_DEFAULT=secret
14+
15+
# Simple single-password mode (overrides ACL; leave blank to use ACL file)
16+
# VC_PASSWORD=
1017

1118
# Registry credentials (only needed for private registries)
1219
# REGISTRY=ghcr.io/yourorg

BUILD.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
## 1. Create the secrets file
2+
3+
```bash
4+
# In PowerShell — run from the project root
5+
# Leave VC_PASSWORD blank to use the ACL file instead
6+
Copy-Item docker\.env docker\.env.local
7+
```
8+
The ACL file at `docker\config\vcache.acl`
9+
already has the correct credentials.
10+
You don't need to set VC_PASSWORD unless you want simple password-only mode.
11+
12+
## 2. Build and start
13+
14+
```bash
15+
# Open PowerShell in the project root folder
16+
# (right-click the folder → "Open in Terminal")
17+
18+
docker compose -f docker\docker-compose.yml up --build -d
19+
```
20+
First build takes 2–3 minutes (downloads Ubuntu base, compiles C source). Subsequent builds are ~20 seconds.
21+
22+
3. Check that all containers are healthy
23+
```bash
24+
docker compose -f docker\docker-compose.yml ps
25+
```
26+
27+
Wait ~30 seconds after start. All four containers (vcache-1, vcache-2, vcache-3, vcache-haproxy) should show healthy or running.
28+
29+
Connect on localhost:6379 (HAProxy) — this is what StackExchange.Redis and RedisInsight should point at.
30+
```
31+
Port map
32+
Port What Who connects here
33+
6379 HAProxy — main entry point Your app RedisInsight redis-cli
34+
8404 HAProxy stats UI Browser → http://localhost:8404/stats (user: admin / pass: admin)
35+
6381 vcache-0 /
36+
```

docker/.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ VC_MAXMEMORY=512m
1515
VC_THREADS=4
1616

1717
# Simple password — set to enable AUTH on all nodes (leave blank to disable)
18-
# VC_PASSWORD=changeme_in_production
18+
# VC_PASSWORD=secret # matches 'default' user in vcache.acl
19+
# VC_PASSWORD_ADMIN=adminpass # matches 'admin' user in vcache.acl
1920

2021
# Set to "1" to run nodes without WAL persistence (faster, no durability)
2122
VC_NO_WAL=0

docker/.env.local

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# .env – VoidCache Docker environment defaults
2+
#
3+
# Copy to .env and edit before running docker compose up.
4+
# All values can also be passed as shell environment variables.
5+
#
6+
# ─────────────────────────────────────────────────────────────────────────────
7+
8+
# Port exposed on the host for the HAProxy load balancer
9+
VC_PORT=6379
10+
11+
# Memory cap per node (supports k, m, g suffixes)
12+
VC_MAXMEMORY=512m
13+
14+
# Worker threads per node (set to CPU cores for best perf)
15+
VC_THREADS=4
16+
17+
# Simple password — set to enable AUTH on all nodes (leave blank to disable)
18+
# VC_PASSWORD=secret # matches 'default' user in vcache.acl
19+
# VC_PASSWORD_ADMIN=adminpass # matches 'admin' user in vcache.acl
20+
21+
# Set to "1" to run nodes without WAL persistence (faster, no durability)
22+
VC_NO_WAL=0

docker/config/vcache.acl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
# Example users:
1717
#
1818
# Full-access admin (password: "adminpass")
19-
admin 3ba05beee8a9d6c6c5e99bf49e6a17eb6d7f9a5ad5b5bfc14f7c5b0a8d1f2c3 *
19+
admin 713bfda78870bf9d1b261f565286f85e97ee614efe5f0faf7c34e7ca4f65baca *
2020
#
2121
# Read-only service account (password: "readonly")
22-
reader e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 r
22+
reader 8171bacf32668a8f44b90087ad107ed63170f57154763ba7e44047bf9e5a7be3 r
2323
#
2424
# Application account with read+write (password: "apppass")
25-
app 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8 rw
25+
app 46400f642c99584e51b14031f875ca9b7b33ccf94ff8d7657197a5bac06f4ffb rw
2626
#
2727
# Default user (no-username AUTH <password>) password: "secret"
2828
default 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b rw

net/commands.c

Lines changed: 182 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,21 +157,46 @@ static void cmd_quit(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
157157
conn->state = VC_CONN_CLOSING;
158158
}
159159

160+
/* RESET — resets connection state. StackExchange.Redis sends this on reconnect. */
161+
static void cmd_reset(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
162+
(void)srv; (void)cmd;
163+
conn->db_idx = 0;
164+
resp_write_simple(WBUF(conn), "RESET");
165+
}
166+
160167
static void cmd_client(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
161168
(void)srv;
162169
if (cmd->argc < 2) { ERR(conn, "ERR", "wrong arguments"); return; }
163170
const char *sub = cmd->argv[1];
164-
if (strcasecmp(sub, "SETNAME") == 0) { OK(conn); return; }
165-
if (strcasecmp(sub, "GETNAME") == 0) { BULK(conn, "", 0); return; }
166-
if (strcasecmp(sub, "ID") == 0) { INT(conn, (int64_t)conn->fd); return; }
171+
if (strcasecmp(sub, "SETNAME") == 0) { OK(conn); return; }
172+
if (strcasecmp(sub, "GETNAME") == 0) { BULK(conn, "", 0); return; }
173+
if (strcasecmp(sub, "ID") == 0) { INT(conn, (int64_t)conn->fd); return; }
174+
/* StackExchange.Redis / RedisInsight no-op stubs */
175+
if (strcasecmp(sub, "NO-EVICT") == 0) { OK(conn); return; }
176+
if (strcasecmp(sub, "NO-TOUCH") == 0) { OK(conn); return; }
177+
if (strcasecmp(sub, "UNPAUSE") == 0) { OK(conn); return; }
178+
if (strcasecmp(sub, "PAUSE") == 0) { OK(conn); return; }
179+
if (strcasecmp(sub, "CACHING") == 0) { OK(conn); return; }
180+
if (strcasecmp(sub, "REPLY") == 0) { OK(conn); return; }
181+
if (strcasecmp(sub, "TRACKING") == 0) { OK(conn); return; }
167182
if (strcasecmp(sub, "INFO") == 0) {
168183
char info[256];
169184
snprintf(info, sizeof(info),
170185
"id=%d\r\naddr=%s:%u\r\ncmd=client\r\n",
171186
conn->fd, conn->peer_addr, conn->peer_port);
172187
BULK(conn, info, strlen(info)); return;
173188
}
174-
ERR(conn, "ERR", "unknown CLIENT subcommand");
189+
if (strcasecmp(sub, "LIST") == 0) {
190+
/* Return minimal CLIENT LIST entry for this connection */
191+
char buf[512];
192+
int n = snprintf(buf, sizeof(buf),
193+
"id=%d addr=%s:%u fd=%d name= age=0 idle=0 flags=N "
194+
"db=0 sub=0 psub=0 multi=-1 rbs=16384 rbp=0 "
195+
"obl=0 oll=0 omem=0 events=r cmd=client resp=2\n",
196+
conn->fd, conn->peer_addr, conn->peer_port, conn->fd);
197+
BULK(conn, buf, (size_t)n); return;
198+
}
199+
resp_write_error(WBUF(conn), "ERR", "unknown CLIENT subcommand");
175200
}
176201

177202
/* ══════════════════════════════════════════════════════════════
@@ -583,6 +608,8 @@ static void cmd_info(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
583608
char info[4096];
584609
int n = snprintf(info, sizeof(info),
585610
"# Server\r\n"
611+
"redis_version:%s\r\n"
612+
"redis_mode:%s\r\n"
586613
"server:voidcache\r\n"
587614
"version:%s\r\n"
588615
"node_id:%s\r\n"
@@ -604,10 +631,29 @@ static void cmd_info(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
604631
"# Keyspace\r\n"
605632
"db0:keys=%llu\r\n"
606633
"\r\n"
634+
"# Replication\r\n"
635+
"role:master\r\n"
636+
"connected_slaves:0\r\n"
637+
"master_failover_state:no-failover\r\n"
638+
"master_replid:0000000000000000000000000000000000000000\r\n"
639+
"master_repl_offset:0\r\n"
640+
"\r\n"
641+
"# Persistence\r\n"
642+
"loading:0\r\n"
643+
"rdb_changes_since_last_save:0\r\n"
644+
"rdb_last_bgsave_status:ok\r\n"
645+
"aof_enabled:0\r\n"
646+
"\r\n"
647+
"# CPU\r\n"
648+
"used_cpu_sys:0\r\n"
649+
"used_cpu_user:0\r\n"
650+
"\r\n"
607651
"# Memory\r\n"
608652
"used_memory:%llu\r\n"
609653
"maxmemory:%llu\r\n",
610654
VC_SERVER_VERSION,
655+
srv->cfg.cluster_enabled ? "cluster" : "standalone",
656+
VC_SERVER_VERSION,
611657
srv->node_id,
612658
srv->cfg.cluster_enabled ? "cluster" : "standalone",
613659
(unsigned long long)atomic_load(&srv->total_conns),
@@ -629,8 +675,44 @@ static void cmd_config(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
629675
(void)srv;
630676
if (cmd->argc < 2) { ERR(conn, "ERR", "wrong arguments"); return; }
631677
if (strcasecmp(cmd->argv[1], "GET") == 0 && cmd->argc >= 3) {
632-
/* Return empty array — no config params exposed */
633-
resp_write_array_header(WBUF(conn), 0);
678+
/* Return a minimal but realistic config for driver compatibility.
679+
* StackExchange.Redis and RedisInsight query these on connect. */
680+
const char *pat = cmd->argv[2];
681+
/* Helper: does the glob pattern match the key? (simple * wildcard) */
682+
#define CFGMATCH(k) (strcmp(pat,"*")==0 || strcasecmp(pat,(k))==0 || (pat[strlen(pat)-1]=='*' && strncasecmp(pat,(k),strlen(pat)-1)==0))
683+
char maxmem[32];
684+
snprintf(maxmem, sizeof(maxmem), "%llu",
685+
(unsigned long long)srv->cache->max_memory);
686+
/* Build array of matched key-value pairs */
687+
struct { const char *k; const char *v; } entries[] = {
688+
{ "hz", "10" },
689+
{ "maxmemory", maxmem },
690+
{ "maxmemory-policy", "allkeys-lru"},
691+
{ "save", "" },
692+
{ "appendonly", "no" },
693+
{ "list-max-ziplist-size", "-2" },
694+
{ "list-compress-depth", "0" },
695+
{ "activerehashing", "yes" },
696+
{ "lazyfree-lazy-eviction", "no" },
697+
{ "latency-tracking", "yes" },
698+
{ "latency-tracking-info-percentiles", "50 99 99.9" },
699+
{ "proto-max-bulk-len", "536870912" },
700+
{ "activedefrag", "no" },
701+
{ "repl-backlog-size", "1048576" },
702+
{ "bind-source-addr", "" },
703+
{ "socket-mark-id", "0" },
704+
};
705+
int nmatch = 0;
706+
for (size_t e = 0; e < sizeof(entries)/sizeof(entries[0]); e++)
707+
if (CFGMATCH(entries[e].k)) nmatch++;
708+
resp_write_array_header(WBUF(conn), nmatch * 2);
709+
for (size_t e = 0; e < sizeof(entries)/sizeof(entries[0]); e++) {
710+
if (CFGMATCH(entries[e].k)) {
711+
BULK(conn, entries[e].k, strlen(entries[e].k));
712+
BULK(conn, entries[e].v, strlen(entries[e].v));
713+
}
714+
}
715+
#undef CFGMATCH
634716
return;
635717
}
636718
if (strcasecmp(cmd->argv[1], "SET") == 0) { OK(conn); return; }
@@ -885,6 +967,91 @@ typedef struct {
885967
uint32_t acl_needed; /* 0 = always allowed (PING/AUTH/HELLO) */
886968
} vc_cmd_entry_t;
887969

970+
/* ── ACL commands (RedisInsight + driver feature detection) ────────────── */
971+
static void cmd_acl(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
972+
(void)srv;
973+
if (cmd->argc < 2) { ERR(conn, "ERR", "wrong arguments"); return; }
974+
const char *sub = cmd->argv[1];
975+
if (strcasecmp(sub, "WHOAMI") == 0) {
976+
const char *name = (conn->user && conn->user->username[0])
977+
? conn->user->username : "default";
978+
BULK(conn, name, strlen(name)); return;
979+
}
980+
if (strcasecmp(sub, "CAT") == 0) {
981+
resp_write_array_header(WBUF(conn), 4);
982+
BULK(conn, "read", 4); BULK(conn, "write", 5);
983+
BULK(conn, "admin", 5); BULK(conn, "pubsub", 6); return;
984+
}
985+
if (strcasecmp(sub, "LIST") == 0) {
986+
resp_write_array_header(WBUF(conn), 1);
987+
const char *name = (conn->user && conn->user->username[0])
988+
? conn->user->username : "default";
989+
char rule[128];
990+
snprintf(rule, sizeof(rule), "user %s on ~* &* +@all", name);
991+
BULK(conn, rule, strlen(rule)); return;
992+
}
993+
if (strcasecmp(sub, "GETUSER") == 0) { resp_write_null_compat(WBUF(conn), conn->resp3); return; }
994+
if (strcasecmp(sub, "SETUSER") == 0 ||
995+
strcasecmp(sub, "DELUSER") == 0 ||
996+
strcasecmp(sub, "LOG") == 0 ||
997+
strcasecmp(sub, "RESET") == 0) { OK(conn); return; }
998+
resp_write_error(WBUF(conn), "ERR", "unknown ACL subcommand");
999+
}
1000+
1001+
/* ── LATENCY (RedisInsight monitoring) ─────────────────────────────────── */
1002+
static void cmd_latency(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
1003+
(void)srv;
1004+
if (cmd->argc < 2) { ERR(conn, "ERR", "wrong arguments"); return; }
1005+
const char *sub = cmd->argv[1];
1006+
if (strcasecmp(sub, "LATEST") == 0) { resp_write_array_header(WBUF(conn), 0); return; }
1007+
if (strcasecmp(sub, "HISTORY") == 0) { resp_write_array_header(WBUF(conn), 0); return; }
1008+
if (strcasecmp(sub, "RESET") == 0) { INT(conn, 0); return; }
1009+
if (strcasecmp(sub, "GRAPH") == 0) { BULK(conn, "", 0); return; }
1010+
resp_write_error(WBUF(conn), "ERR", "unknown LATENCY subcommand");
1011+
}
1012+
1013+
/* ── SLOWLOG (RedisInsight) ─────────────────────────────────────────────── */
1014+
static void cmd_slowlog(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
1015+
(void)srv;
1016+
if (cmd->argc < 2) { ERR(conn, "ERR", "wrong arguments"); return; }
1017+
const char *sub = cmd->argv[1];
1018+
if (strcasecmp(sub, "GET") == 0) { resp_write_array_header(WBUF(conn), 0); return; }
1019+
if (strcasecmp(sub, "LEN") == 0) { INT(conn, 0); return; }
1020+
if (strcasecmp(sub, "RESET") == 0) { OK(conn); return; }
1021+
resp_write_error(WBUF(conn), "ERR", "unknown SLOWLOG subcommand");
1022+
}
1023+
1024+
/* ── OBJECT (RedisInsight key inspector) ────────────────────────────────── */
1025+
static void cmd_object(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
1026+
(void)srv;
1027+
if (cmd->argc < 2) { ERR(conn, "ERR", "wrong arguments"); return; }
1028+
const char *sub = cmd->argv[1];
1029+
if (strcasecmp(sub, "ENCODING") == 0) { BULK(conn, "embstr", 7); return; }
1030+
if (strcasecmp(sub, "REFCOUNT") == 0) { INT(conn, 1); return; }
1031+
if (strcasecmp(sub, "IDLETIME") == 0) { INT(conn, 0); return; }
1032+
if (strcasecmp(sub, "FREQ") == 0) { INT(conn, 0); return; }
1033+
if (strcasecmp(sub, "HELP") == 0) { resp_write_array_header(WBUF(conn), 0); return; }
1034+
resp_write_error(WBUF(conn), "ERR", "unknown OBJECT subcommand");
1035+
}
1036+
1037+
/* ── WAIT (SE.Redis replication sync) ──────────────────────────────────── */
1038+
static void cmd_wait(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
1039+
(void)srv; (void)cmd;
1040+
INT(conn, 0); /* single-node: 0 replicas */
1041+
}
1042+
1043+
/* ── XLEN stub (Streams feature detection) ──────────────────────────────── */
1044+
static void cmd_xlen(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
1045+
(void)srv; (void)cmd;
1046+
ERR(conn, "WRONGTYPE", "Operation against a key holding the wrong kind of value");
1047+
}
1048+
1049+
/* ── SRANDMEMBER stub ───────────────────────────────────────────────────── */
1050+
static void cmd_srandmember(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
1051+
(void)srv; (void)cmd;
1052+
ERR(conn, "WRONGTYPE", "Operation against a key holding the wrong kind of value");
1053+
}
1054+
8881055
static const vc_cmd_entry_t cmd_table[] = {
8891056
/* Connection */
8901057
{ "PING", cmd_ping, 0 },
@@ -928,6 +1095,15 @@ static const vc_cmd_entry_t cmd_table[] = {
9281095
{ "VCSET", cmd_vcset, VC_ACL_WRITE },
9291096
{ "VCGET", cmd_vcget, VC_ACL_READ },
9301097
{ "VCINFO", cmd_vcinfo, VC_ACL_READ },
1098+
/* StackExchange.Redis + RedisInsight compatibility */
1099+
{ "RESET", cmd_reset, 0 },
1100+
{ "ACL", cmd_acl, 0 },
1101+
{ "LATENCY", cmd_latency, VC_ACL_ADMIN },
1102+
{ "SLOWLOG", cmd_slowlog, VC_ACL_ADMIN },
1103+
{ "OBJECT", cmd_object, VC_ACL_READ },
1104+
{ "WAIT", cmd_wait, 0 },
1105+
{ "XLEN", cmd_xlen, VC_ACL_READ },
1106+
{ "SRANDMEMBER", cmd_srandmember, VC_ACL_READ },
9311107
};
9321108

9331109
#define CMD_TABLE_SIZE (sizeof(cmd_table) / sizeof(cmd_table[0]))

net/proto.c

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,24 +127,34 @@ resp_parse_result_t resp_parse_command(const char *buf, size_t len,
127127

128128
#define NEED(n) do { if ((size_t)(end - p) < (size_t)(n)) return RESP_PARSE_MORE; } while(0)
129129

130-
/* ── Inline command (telnet-friendly) ── */
130+
/* ── Inline command (telnet-friendly, handles \r\n and bare \n) ── */
131131
if (*p != '*') {
132-
const char *nl = find_crlf(p, (size_t)(end - p));
132+
/* Find end of line — accept both \r\n and bare \n */
133+
const char *nl = NULL;
134+
size_t nl_skip = 2; /* bytes to skip past the line ending */
135+
for (const char *q = p; q < end; q++) {
136+
if (*q == '\r' && q + 1 < end && *(q+1) == '\n') {
137+
nl = q; nl_skip = 2; break;
138+
}
139+
if (*q == '\n') {
140+
nl = q; nl_skip = 1; break;
141+
}
142+
}
133143
if (!nl) return RESP_PARSE_MORE;
134-
/* Split on spaces */
144+
/* Split on spaces/tabs */
135145
const char *s = p;
136146
while (s < nl && cmd->argc < VC_MAX_ARGS) {
137-
while (s < nl && *s == ' ') s++;
147+
while (s < nl && (*s == ' ' || *s == '\t')) s++;
138148
if (s >= nl) break;
139149
const char *e2 = s;
140-
while (e2 < nl && *e2 != ' ') e2++;
150+
while (e2 < nl && *e2 != ' ' && *e2 != '\t') e2++;
141151
size_t alen = (size_t)(e2 - s);
142152
cmd->argv[cmd->argc] = strndup(s, alen);
143153
cmd->argl[cmd->argc] = alen;
144154
cmd->argc++;
145155
s = e2;
146156
}
147-
*consumed = (size_t)(nl - buf) + 2;
157+
*consumed = (size_t)(nl - buf) + nl_skip;
148158
return (cmd->argc > 0) ? RESP_PARSE_OK : RESP_PARSE_ERR;
149159
}
150160

0 commit comments

Comments
 (0)