testkv.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. // Package testkv provides an in-memory PizzaKV-compatible server for tests. It
  2. // speaks the PKBFI wire protocol over a real TCP listener so production
  3. // KVPool/KVClient code paths can be exercised without a separate binary.
  4. package testkv
  5. import (
  6. "bufio"
  7. "hash/crc32"
  8. "io"
  9. "net"
  10. "sort"
  11. "strings"
  12. "sync"
  13. "testing"
  14. "time"
  15. "github.com/danfragoso/pizzasql-next/pkg/storage"
  16. )
  17. const (
  18. headerMagic = "PKBF"
  19. headerVersion = 1
  20. headerSize = 32
  21. opPing = 1
  22. opGet = 3
  23. opPut = 4
  24. opDelete = 5
  25. opExists = 6
  26. opMultiGet = 7
  27. opBatchWrite = 8
  28. opScanOpen = 9
  29. opScanNext = 10
  30. opScanClose = 11
  31. opCompareBatch = 12
  32. batchPut = 1
  33. batchDelete = 2
  34. statusOK = 0
  35. statusNotFound = 1
  36. statusError = 2
  37. )
  38. var crcTable = crc32.MakeTable(crc32.Castagnoli)
  39. type scan struct {
  40. keys []string
  41. offset int
  42. limit uint32
  43. keysOnly bool
  44. }
  45. // Server is an in-memory KV server.
  46. type Server struct {
  47. mu sync.Mutex
  48. data map[string][]byte
  49. lsns map[string]uint64
  50. nextLSN uint64
  51. requests map[uint16]int
  52. ln net.Listener
  53. wg sync.WaitGroup
  54. closed bool
  55. scans map[uint64]*scan
  56. nextScan uint64
  57. }
  58. // New starts an in-memory KV server on an ephemeral TCP port.
  59. func New(t testing.TB) *Server {
  60. t.Helper()
  61. ln, err := net.Listen("tcp", "127.0.0.1:0")
  62. if err != nil {
  63. t.Fatal(err)
  64. }
  65. s := &Server{
  66. data: make(map[string][]byte),
  67. lsns: make(map[string]uint64),
  68. ln: ln,
  69. scans: make(map[uint64]*scan),
  70. }
  71. s.wg.Add(1)
  72. go s.acceptLoop()
  73. t.Cleanup(func() { s.Close() })
  74. return s
  75. }
  76. // Addr returns the server's listen address.
  77. func (s *Server) Addr() string { return s.ln.Addr().String() }
  78. // Pool creates a KV pool connected to the server.
  79. func (s *Server) Pool(size int) *storage.KVPool {
  80. pool, err := storage.NewKVPool(s.Addr(), size, 5*time.Second)
  81. if err != nil {
  82. panic(err)
  83. }
  84. return pool
  85. }
  86. // Close stops the server.
  87. func (s *Server) Close() {
  88. s.mu.Lock()
  89. if s.closed {
  90. s.mu.Unlock()
  91. return
  92. }
  93. s.closed = true
  94. s.mu.Unlock()
  95. _ = s.ln.Close()
  96. s.wg.Wait()
  97. }
  98. func (s *Server) acceptLoop() {
  99. defer s.wg.Done()
  100. for {
  101. conn, err := s.ln.Accept()
  102. if err != nil {
  103. return
  104. }
  105. s.wg.Add(1)
  106. go func() {
  107. defer s.wg.Done()
  108. s.handle(conn)
  109. }()
  110. }
  111. }
  112. func (s *Server) handle(conn net.Conn) {
  113. defer conn.Close()
  114. r := bufio.NewReader(conn)
  115. for {
  116. opcode, _, requestID, payload, err := readFrame(r)
  117. if err != nil {
  118. return
  119. }
  120. body := s.execute(opcode, payload)
  121. if _, err := conn.Write(encodeResponse(opcode, requestID, body)); err != nil {
  122. return
  123. }
  124. }
  125. }
  126. // RequestCount reports observed PKBFI requests for bounded-work assertions.
  127. func (s *Server) RequestCount(opcode uint16) int {
  128. s.mu.Lock()
  129. defer s.mu.Unlock()
  130. return s.requests[opcode]
  131. }
  132. func (s *Server) execute(opcode uint16, payload []byte) []byte {
  133. s.mu.Lock()
  134. if s.requests == nil {
  135. s.requests = make(map[uint16]int)
  136. }
  137. s.requests[opcode]++
  138. s.mu.Unlock()
  139. switch opcode {
  140. case opPing:
  141. return statusOKBody(nil)
  142. case opGet:
  143. key, ok := parseOneKey(payload)
  144. if !ok {
  145. return errorBody("InvalidPayload")
  146. }
  147. s.mu.Lock()
  148. v, found := s.data[string(key)]
  149. lsn := s.lsns[string(key)]
  150. s.mu.Unlock()
  151. if !found {
  152. return statusBody(statusNotFound, nil)
  153. }
  154. body := make([]byte, 14+len(v))
  155. putU16(body[0:2], statusOK)
  156. putU64(body[2:10], lsn)
  157. putU32(body[10:14], uint32(len(v)))
  158. copy(body[14:], v)
  159. return body
  160. case opPut:
  161. key, value, ok := parsePut(payload)
  162. if !ok {
  163. return errorBody("InvalidPayload")
  164. }
  165. s.mu.Lock()
  166. s.nextLSN++
  167. lsn := s.nextLSN
  168. s.data[string(key)] = append([]byte(nil), value...)
  169. s.lsns[string(key)] = lsn
  170. s.mu.Unlock()
  171. body := make([]byte, 10)
  172. putU16(body[0:2], statusOK)
  173. putU64(body[2:10], lsn)
  174. return body
  175. case opDelete:
  176. key, ok := parseOneKey(payload)
  177. if !ok {
  178. return errorBody("InvalidPayload")
  179. }
  180. s.mu.Lock()
  181. _, found := s.data[string(key)]
  182. delete(s.data, string(key))
  183. delete(s.lsns, string(key))
  184. s.mu.Unlock()
  185. body := make([]byte, 3)
  186. putU16(body[0:2], statusOK)
  187. if found {
  188. body[2] = 1
  189. }
  190. return body
  191. case opExists:
  192. key, ok := parseOneKey(payload)
  193. if !ok {
  194. return errorBody("InvalidPayload")
  195. }
  196. s.mu.Lock()
  197. _, found := s.data[string(key)]
  198. s.mu.Unlock()
  199. body := make([]byte, 3)
  200. putU16(body[0:2], statusOK)
  201. if found {
  202. body[2] = 1
  203. }
  204. return body
  205. case opMultiGet:
  206. keys, ok := parseMultiGet(payload)
  207. if !ok {
  208. return errorBody("InvalidPayload")
  209. }
  210. body := make([]byte, 6)
  211. putU16(body[0:2], statusOK)
  212. putU32(body[2:6], uint32(len(keys)))
  213. s.mu.Lock()
  214. for _, key := range keys {
  215. v, found := s.data[string(key)]
  216. if !found {
  217. body = append(body, make([]byte, 16)...)
  218. continue
  219. }
  220. entry := make([]byte, 16+len(v))
  221. entry[0] = 1
  222. putU32(entry[4:8], uint32(len(v)))
  223. putU64(entry[8:16], s.lsns[string(key)])
  224. copy(entry[16:], v)
  225. body = append(body, entry...)
  226. }
  227. s.mu.Unlock()
  228. return body
  229. case opBatchWrite:
  230. ops, ok := parseBatchOps(payload)
  231. if !ok {
  232. return errorBody("InvalidPayload")
  233. }
  234. s.mu.Lock()
  235. s.nextLSN++
  236. lsn := s.nextLSN
  237. applyOps(s.data, s.lsns, ops, lsn)
  238. s.mu.Unlock()
  239. body := make([]byte, 10)
  240. putU16(body[0:2], statusOK)
  241. putU64(body[2:10], lsn)
  242. return body
  243. case opCompareBatch:
  244. checks, ops, ok := parseCompareBatch(payload)
  245. if !ok {
  246. return errorBody("InvalidPayload")
  247. }
  248. s.mu.Lock()
  249. committed := true
  250. for _, c := range checks {
  251. if c.LSN == 0 {
  252. if _, found := s.data[string(c.Key)]; found {
  253. committed = false
  254. break
  255. }
  256. } else if s.lsns[string(c.Key)] != c.LSN {
  257. committed = false
  258. break
  259. }
  260. }
  261. var lsn uint64
  262. if committed {
  263. s.nextLSN++
  264. lsn = s.nextLSN
  265. applyOps(s.data, s.lsns, ops, lsn)
  266. }
  267. s.mu.Unlock()
  268. body := make([]byte, 18)
  269. putU16(body[0:2], statusOK)
  270. if committed {
  271. body[2] = 1
  272. }
  273. putU64(body[10:18], lsn)
  274. return body
  275. case opScanOpen:
  276. includeValues, limit, prefix, ok := parseScanOpen(payload)
  277. if !ok {
  278. return errorBody("InvalidPayload")
  279. }
  280. s.mu.Lock()
  281. keys := make([]string, 0)
  282. for k := range s.data {
  283. if strings.HasPrefix(k, string(prefix)) {
  284. keys = append(keys, k)
  285. }
  286. }
  287. sort.Strings(keys)
  288. s.nextScan++
  289. id := s.nextScan
  290. s.scans[id] = &scan{keys: keys, limit: limit, keysOnly: !includeValues}
  291. s.mu.Unlock()
  292. body := make([]byte, 10)
  293. putU16(body[0:2], statusOK)
  294. putU64(body[2:10], id)
  295. return body
  296. case opScanNext:
  297. id, ok := parseScanID(payload)
  298. if !ok {
  299. return errorBody("InvalidPayload")
  300. }
  301. s.mu.Lock()
  302. sc := s.scans[id]
  303. if sc == nil {
  304. s.mu.Unlock()
  305. return errorBody("ScanNotFound")
  306. }
  307. remaining := len(sc.keys) - sc.offset
  308. count := int(sc.limit)
  309. if count > remaining {
  310. count = remaining
  311. }
  312. end := sc.offset + count
  313. body := make([]byte, 10)
  314. putU16(body[0:2], statusOK)
  315. if end >= len(sc.keys) {
  316. body[2] = 1
  317. }
  318. putU32(body[6:10], uint32(count))
  319. for _, key := range sc.keys[sc.offset:end] {
  320. value := s.data[key]
  321. if sc.keysOnly {
  322. value = nil
  323. }
  324. entry := make([]byte, 16+len(key)+len(value))
  325. putU32(entry[0:4], uint32(len(key)))
  326. putU32(entry[4:8], uint32(len(value)))
  327. putU64(entry[8:16], s.lsns[key])
  328. copy(entry[16:], key)
  329. copy(entry[16+len(key):], value)
  330. body = append(body, entry...)
  331. }
  332. sc.offset = end
  333. s.mu.Unlock()
  334. return body
  335. case opScanClose:
  336. id, ok := parseScanID(payload)
  337. if !ok {
  338. return errorBody("InvalidPayload")
  339. }
  340. s.mu.Lock()
  341. delete(s.scans, id)
  342. s.mu.Unlock()
  343. body := make([]byte, 3)
  344. putU16(body[0:2], statusOK)
  345. body[2] = 1
  346. return body
  347. default:
  348. return errorBody("UnknownOpcode")
  349. }
  350. }
  351. func applyOps(data map[string][]byte, lsns map[string]uint64, ops []storage.BatchOp, lsn uint64) {
  352. for _, op := range ops {
  353. if op.Op == batchPut {
  354. data[string(op.Key)] = append([]byte(nil), op.Value...)
  355. lsns[string(op.Key)] = lsn
  356. } else {
  357. delete(data, string(op.Key))
  358. delete(lsns, string(op.Key))
  359. }
  360. }
  361. }
  362. // ── wire helpers ────────────────────────────────────────────────────────────
  363. func crc32c(p []byte) uint32 { return crc32.Checksum(p, crcTable) }
  364. func putU16(b []byte, v uint16) { b[0] = byte(v); b[1] = byte(v >> 8) }
  365. func putU32(b []byte, v uint32) {
  366. b[0] = byte(v)
  367. b[1] = byte(v >> 8)
  368. b[2] = byte(v >> 16)
  369. b[3] = byte(v >> 24)
  370. }
  371. func putU64(b []byte, v uint64) {
  372. b[0] = byte(v)
  373. b[1] = byte(v >> 8)
  374. b[2] = byte(v >> 16)
  375. b[3] = byte(v >> 24)
  376. b[4] = byte(v >> 32)
  377. b[5] = byte(v >> 40)
  378. b[6] = byte(v >> 48)
  379. b[7] = byte(v >> 56)
  380. }
  381. func getU16(b []byte) uint16 { return uint16(b[0]) | uint16(b[1])<<8 }
  382. func getU32(b []byte) uint32 {
  383. return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
  384. }
  385. func getU64(b []byte) uint64 {
  386. return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
  387. uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
  388. }
  389. func encodeFrame(opcode, flags uint16, requestID uint64, payload []byte) []byte {
  390. frame := make([]byte, headerSize+len(payload))
  391. copy(frame[0:4], headerMagic)
  392. putU16(frame[4:6], headerVersion)
  393. putU16(frame[8:10], opcode)
  394. putU16(frame[10:12], flags)
  395. putU64(frame[12:20], requestID)
  396. putU32(frame[20:24], uint32(len(payload)))
  397. putU32(frame[24:28], crc32c(payload))
  398. putU32(frame[28:32], crc32c(frame[0:32]))
  399. copy(frame[32:], payload)
  400. return frame
  401. }
  402. func encodeResponse(opcode uint16, requestID uint64, body []byte) []byte {
  403. return encodeFrame(opcode|0x8000, 1, requestID, body)
  404. }
  405. func readFrame(r *bufio.Reader) (uint16, uint16, uint64, []byte, error) {
  406. var header [headerSize]byte
  407. if _, err := io.ReadFull(r, header[:]); err != nil {
  408. return 0, 0, 0, nil, err
  409. }
  410. payloadLen := getU32(header[20:24])
  411. payload := make([]byte, payloadLen)
  412. if _, err := io.ReadFull(r, payload); err != nil {
  413. return 0, 0, 0, nil, err
  414. }
  415. return getU16(header[8:10]), getU16(header[10:12]), getU64(header[12:20]), payload, nil
  416. }
  417. func statusOKBody(p []byte) []byte {
  418. body := make([]byte, 2+len(p))
  419. putU16(body[0:2], statusOK)
  420. copy(body[2:], p)
  421. return body
  422. }
  423. func statusBody(status uint16, p []byte) []byte {
  424. body := make([]byte, 2+len(p))
  425. putU16(body[0:2], status)
  426. copy(body[2:], p)
  427. return body
  428. }
  429. func errorBody(msg string) []byte {
  430. body := make([]byte, 2+len(msg))
  431. putU16(body[0:2], statusError)
  432. copy(body[2:], msg)
  433. return body
  434. }
  435. func parseOneKey(p []byte) ([]byte, bool) {
  436. if len(p) < 4 {
  437. return nil, false
  438. }
  439. l := getU32(p[0:4])
  440. if 4+int(l) != len(p) {
  441. return nil, false
  442. }
  443. return p[4:], true
  444. }
  445. func parsePut(p []byte) ([]byte, []byte, bool) {
  446. if len(p) < 8 {
  447. return nil, nil, false
  448. }
  449. kl := getU32(p[0:4])
  450. vl := getU32(p[4:8])
  451. if 8+int(kl)+int(vl) != len(p) {
  452. return nil, nil, false
  453. }
  454. return p[8 : 8+kl], p[8+kl:], true
  455. }
  456. func parseMultiGet(p []byte) ([][]byte, bool) {
  457. if len(p) < 4 {
  458. return nil, false
  459. }
  460. n := getU32(p[0:4])
  461. keys := make([][]byte, 0, n)
  462. pos := 4
  463. for i := uint32(0); i < n; i++ {
  464. if len(p)-pos < 4 {
  465. return nil, false
  466. }
  467. l := getU32(p[pos : pos+4])
  468. pos += 4
  469. if len(p)-pos < int(l) {
  470. return nil, false
  471. }
  472. keys = append(keys, p[pos:pos+int(l)])
  473. pos += int(l)
  474. }
  475. return keys, pos == len(p)
  476. }
  477. func parseBatchOps(p []byte) ([]storage.BatchOp, bool) {
  478. if len(p) < 8 {
  479. return nil, false
  480. }
  481. n := getU32(p[0:4])
  482. ml := getU32(p[4:8])
  483. pos := 8 + int(ml)
  484. ops := make([]storage.BatchOp, 0, n)
  485. for i := uint32(0); i < n; i++ {
  486. if len(p)-pos < 12 {
  487. return nil, false
  488. }
  489. op := p[pos]
  490. kl := getU32(p[pos+4 : pos+8])
  491. vl := getU32(p[pos+8 : pos+12])
  492. pos += 12
  493. if op != batchPut && op != batchDelete {
  494. return nil, false
  495. }
  496. if len(p)-pos < int(kl)+int(vl) {
  497. return nil, false
  498. }
  499. key := p[pos : pos+int(kl)]
  500. pos += int(kl)
  501. value := p[pos : pos+int(vl)]
  502. pos += int(vl)
  503. ops = append(ops, storage.BatchOp{Op: op, Key: key, Value: value})
  504. }
  505. return ops, pos == len(p)
  506. }
  507. func parseCompareBatch(p []byte) ([]storage.CompareCheck, []storage.BatchOp, bool) {
  508. if len(p) < 16 {
  509. return nil, nil, false
  510. }
  511. nc := getU32(p[0:4])
  512. no := getU32(p[4:8])
  513. ml := getU32(p[8:12])
  514. pos := 16
  515. checks := make([]storage.CompareCheck, 0, nc)
  516. for i := uint32(0); i < nc; i++ {
  517. if len(p)-pos < 16 {
  518. return nil, nil, false
  519. }
  520. kl := getU32(p[pos : pos+4])
  521. lsn := getU64(p[pos+8 : pos+16])
  522. pos += 16
  523. if len(p)-pos < int(kl) {
  524. return nil, nil, false
  525. }
  526. checks = append(checks, storage.CompareCheck{Key: p[pos : pos+int(kl)], LSN: lsn})
  527. pos += int(kl)
  528. }
  529. pos += int(ml)
  530. ops := make([]storage.BatchOp, 0, no)
  531. for i := uint32(0); i < no; i++ {
  532. if len(p)-pos < 12 {
  533. return nil, nil, false
  534. }
  535. op := p[pos]
  536. kl := getU32(p[pos+4 : pos+8])
  537. vl := getU32(p[pos+8 : pos+12])
  538. pos += 12
  539. if op != batchPut && op != batchDelete {
  540. return nil, nil, false
  541. }
  542. if len(p)-pos < int(kl)+int(vl) {
  543. return nil, nil, false
  544. }
  545. key := p[pos : pos+int(kl)]
  546. pos += int(kl)
  547. value := p[pos : pos+int(vl)]
  548. pos += int(vl)
  549. ops = append(ops, storage.BatchOp{Op: op, Key: key, Value: value})
  550. }
  551. return checks, ops, pos == len(p)
  552. }
  553. func parseScanOpen(p []byte) (bool, uint32, []byte, bool) {
  554. if len(p) < 12 {
  555. return false, 0, nil, false
  556. }
  557. include := p[0] != 0
  558. limit := getU32(p[4:8])
  559. pl := getU32(p[8:12])
  560. if 12+int(pl) != len(p) {
  561. return false, 0, nil, false
  562. }
  563. return include, limit, p[12:], true
  564. }
  565. func parseScanID(p []byte) (uint64, bool) {
  566. if len(p) < 8 {
  567. return 0, false
  568. }
  569. return getU64(p[0:8]), true
  570. }