Skip to content

Commit 0ac07dd

Browse files
committed
Add persistence and some new commands:
* Stores commands in an AOF using WAL method * Saves and restores a DB from disk * Replays the AOF after restoring the DB * Add 'SAVE' and 'BGSAVE' commands * Add 'CLIENT' command * Add 'PING' command * Add 'EXISTS' command * Add 'GETSET' command * Add 'CONVERT' command * Remove 'LOLWUT' command * Run BGSAVE as a scheduled task * Add statistics for 'persistence' * Add option to save the DB in binary format * Add and fix unit tests * Add timestamp to output and cleanup error messages etc * Don't require IDENT to be sent by the client during auth * Fix problem where messages were lost, abort a client connection that takes too long (60s) * Major optimization, RPUSH with multiple elements is O(N) instead of O(2^N) * Other minor fixes and code cleanup
1 parent 3132b8e commit 0ac07dd

File tree

15 files changed

+768
-301
lines changed

15 files changed

+768
-301
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
.modules/
2+
*.aof
3+
*.db
4+
*.lock
5+
*.old
6+
*.bin

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
# Changelog
22

3+
## 0.14.0 (2020-07-27)
4+
5+
* Fix bug where client IP is not saved in client list
6+
* Fix bug where client would not disconnect when sent a `kill` command
7+
* Add persistence which writes commands in an AOF using WAL method
8+
* Saves and restores a DB from disk
9+
* Replays the AOF after restoring the DB
10+
* Add 'SAVE' and 'BGSAVE' commands
11+
* Add 'CLIENT' command
12+
* Add 'PING' command
13+
* Add 'EXISTS' command
14+
* Add 'GETSET' command
15+
* Add 'CONVERT' command
16+
* Remove 'LOLWUT' command
17+
* Run BGSAVE as a scheduled task
18+
* Add statistics for 'persistence'
19+
* Add option to save the DB in binary format
20+
* Add and fix unit tests
21+
* Add timestamp to output and cleanup error messages etc
22+
* Don't require IDENT to be sent by the client during auth
23+
* Fix problem where messages were lost, abort a client connection that takes too long (60s)
24+
* Major optimization, RPUSH with multiple elements is O(N) instead of O(2^N)
25+
* Other minor fixes and code cleanup
26+
327
## 0.13.0 (2020-06-27)
428

529
* Fix issue where child process doesn't actually 'exit' when exiting (in a fork)

README.md

Lines changed: 155 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Redis-inspired key/value store written in PicoLisp
22

3-
This program mimics functionality of a [Redis](https://redis.io) in-memory database, but is designed specifically for [PicoLisp](https://picolisp.com) applications without on-disk persistence.
3+
This program mimics functionality of a [Redis](https://redis.io) in-memory database, but is designed specifically for [PicoLisp](https://picolisp.com) applications with optional on-disk persistence.
44

55
The included `server.l` and `client.l` can be used to send and receive _"Redis-like"_ commands over TCP or UNIX named pipess.
66

@@ -11,14 +11,15 @@ The included `server.l` and `client.l` can be used to send and receive _"Redis-l
1111
3. [Usage](#usage)
1212
4. [Note and Limitations](#notes-and-limitations)
1313
5. [How it works](#how-it-works)
14-
6. [Testing](#testing)
15-
7. [Contributing](#contributing)
16-
8. [Changelog](#changelog)
17-
9. [License](#license)
14+
6. [Persistence](#persistence)
15+
7. [Testing](#testing)
16+
8. [Contributing](#contributing)
17+
9. [Changelog](#changelog)
18+
10. [License](#license)
1819

1920
# Requirements
2021

21-
* PicoLisp 32-bit/64-bit `v17.12` to `v20.5.26`
22+
* PicoLisp 32-bit/64-bit `v17.12` to `v20.6.29`
2223
* Linux or UNIX-like OS (with support for named pipes)
2324

2425
# Getting Started
@@ -47,8 +48,7 @@ That should return some interesting info about your server. See below for more e
4748
1. Load the client library in your project: `(load "libkvclient.l")`
4849
2. Set the server password `(setq *KV_pass "yourpass")`
4950
3. Start the client listener with `(kv-start-client)`
50-
5. Optionally send your client's identity with key/value pairs `(kv-identify "location" "Tokyo" "building" "109")`
51-
6. Send your command and arguments with `(kv-send-data '("INFO" "server"))`
51+
4. Send your command and arguments with `(kv-send-data '("INFO" "server"))`
5252

5353
Received data will be returned as-is (list, integer, string, etc). Wrap the result like: `(kv-print Result)` to send the output to `STDOUT`:
5454

@@ -59,8 +59,6 @@ Received data will be returned as-is (list, integer, string, etc). Wrap the resu
5959
-> "yourpass"
6060
: (kv-start-client)
6161
-> T
62-
: (kv-identify "key1" "value2" "key2" "value3")
63-
-> "OK 35F2F81D"
6462
: (kv-send-data '("set" "mykey" 12345))
6563
-> "OK"
6664
: (kv-send-data '("get" "mykey"))
@@ -81,18 +79,20 @@ This section describes usage information for the CLI tools `server.l` and `clien
8179

8280
## Server
8381

84-
The server listens in the foreground for TCP connections on port `6378` by default. Only the `password`, `port`, and `verbosity` are configurable, and a `password` is required:
82+
The server listens in the foreground for TCP connections on port `6378` by default. Only the `password`, `port`, `persistence`, and `verbosity` are configurable, and a `password` is required:
8583

8684
```
8785
# server.l
8886
Usage: ./server.l --pass <pass> [options]
8987
90-
Example: ./server.l --pass foobared --port 6378 --verbose'
88+
Example: ./server.l --pass foobared --port 6378 --verbose --persist 60
9189
9290
Options:
9391
--help Show this help message and exit
9492
93+
--binary Store data in binary format instead of text (default: plaintext)
9594
--pass <password> Password used by clients to access the server (required)
95+
--persist <seconds> Number of seconds between database persists to disk (default: disabled)
9696
--port <port> TCP listen port for communication with clients (default: 6378)
9797
--verbose Verbose flag (default: False)
9898
```
@@ -122,12 +122,12 @@ The client handles authentication, identification, and sending of _"Redis-like"_
122122
# client.l
123123
Usage: ./client.l --pass <pass> COMMAND [arguments]
124124
125-
Example: ./client.l --pass foobared --port 6378 INFO server'
125+
Example: ./client.l --pass foobared --port 6378 INFO server
126126
127127
Options:
128128
--help Show this help message and exit
129129
130-
--id <id> Uniquely identifiable client ID (default: randomly generated)
130+
--name <name> Easily identifiable client name (default: randomly generated)
131131
--host <host> Hostname or IP of the key/value server (default: localhost)
132132
--pass <data> Password used to access the server (required)
133133
--poll <seconds> Number of seconds for polling the key/value server (default: don't poll)
@@ -136,23 +136,30 @@ Options:
136136
COMMAND LIST Commands are case-insensitive and don't always require arguments.
137137
Examples:
138138
139-
DEL key [key ..] DEL key1 key2 key3
140-
GET key GET key1
141-
INFO [section] INFO memory
142-
LINDEX key index LINDEX mylist 0
143-
LLEN key LLEN mylist
144-
LOLWUT number LOLWUT 5
145-
LPOP key LPOP mylist
146-
LPOPRPUSH source destination LPOPRPUSH mylist myotherlist
147-
RPUSH key element [element ..] RPUSH mylist task1 task2 task3
148-
SET key value SET mykey hello
139+
BGSAVE BGSAVE
140+
CLIENT ID|KILL|LIST id [id ..] CLIENT LIST
141+
CONVERT CONVERT
142+
DEL key [key ..] DEL key1 key2 key3
143+
EXISTS key [key ..] EXISTS key1 key2 key3
144+
GET key GET key1
145+
GETSET key value GETSET mykey hello
146+
INFO [section] INFO memory
147+
LINDEX key index LINDEX mylist 0
148+
LLEN key LLEN mylist
149+
LPOP key LPOP mylist
150+
LPOPRPUSH source destination LPOPRPUSH mylist myotherlist
151+
PING [message] PING hello
152+
RPUSH key element [element ..] RPUSH mylist task1 task2 task3
153+
SAVE SAVE
154+
SET key value SET mykey hello
149155
```
150156

151-
The `COMMANDS` take the exact same arguments as their respective [Redis commands](https://redis.io/commands).
157+
Most `COMMANDS` take the exact same arguments as their respective [Redis commands](https://redis.io/commands).
152158

153159
### Examples
154160

155161
```
162+
# Obtain information about the server
156163
./client.l --pass yourpass INFO server
157164
OK 37D13779
158165
@@ -166,14 +173,27 @@ uptime_in_seconds:1
166173
uptime_in_days:0
167174
executable:/usr/bin/picolisp
168175
176+
# Set a key
169177
./client.l --pass yourpass SET mykey myvalue
170178
OK 53E02FC6
171179
OK
172180
181+
# Get a key
173182
./client.l --pass yourpass GET mykey
174183
OK 40E83305
175184
myvalue
176185
186+
# Get a key, then set it
187+
./client.l --pass yourpass GETSET mykey yourvalue
188+
OK 69E88646
189+
myvalue
190+
191+
# Check if a key exists
192+
./client.l --pass yourpass EXISTS mykey
193+
OK 43BFA2C
194+
1
195+
196+
# Delete a key
177197
./client.l --pass yourpass DEL mykey
178198
OK 4C2B6088
179199
1
@@ -182,33 +202,63 @@ OK 4C2B6088
182202
OK 11242B95
183203
no data
184204
185-
./client.l --pass yourpass --id 11242B95 RPUSH mylist task1 task2 task3
205+
./client.l --pass yourpass EXISTS mykey
206+
OK 5F1E8D78
207+
0
208+
209+
# Add multiple values to a key (a list)
210+
./client.l --pass yourpass --name 11242B95 RPUSH mylist task1 task2 task3
186211
OK 11242B95
187212
3
188213
189214
./client.l --pass yourpass RPUSH mylist task4 task5
190215
OK 4E7E0FC3
191216
5
192217
218+
# Left pop a value from the head of a list
193219
./client.l --pass yourpass LPOP mylist
194220
OK 258514BF
195221
task1
196222
223+
# Check how many values are in a key (a list)
197224
./client.l --pass yourpass LLEN mylist
198225
OK 107CF205
199226
4
200227
228+
# Left pop a value from the head of a list, push it to the tail of another list
201229
./client.l --pass yourpass LPOPRPUSH mylist mynewlist
202230
OK 46028880
203231
task2
204232
233+
# Get the value of a key (a list) using a zero-based index
205234
./client.l --pass yourpass LINDEX mynewlist -1
206235
OK 129AE0F8
207236
task2
208237
209-
./client.l --pass yourpass LOLWUT 1
210-
OK 71FE650B
211-
█▆▆▄▁▂▄▂▅▄▂█▁▄▂▅▃▆▂█▃▅▃▄▆▄▇█▇▇▃▃█▅▃▇▄▄▃▇▄▃▇▂▂▄▆▃██▂▄▄█▆▃▅▅▃▅▂▃▄▃▇▇▇▄▄▄▄▂▄▆▁▂▅▁▄▆
238+
# Ping the server
239+
./client.l --pass yourpass PING
240+
OK 6DCE69EB
241+
PONG
242+
243+
# Ping the server with a custom message
244+
./client.l --pass yourpass PING "Hello"
245+
OK 6F02D9DC
246+
Hello
247+
248+
# Save the database in the foreground (blocking)
249+
./client.l --pass yourpass SAVE
250+
OK 1F60EABE
251+
OK
252+
253+
# Save the database in the background (non-blocking)
254+
./client.l --pass yourpass BGSAVE
255+
OK 1270937D
256+
Background saving started
257+
258+
# Convert the database from plaintext to binary, or binary to plaintext
259+
./client.l --pass yourpass CONVERT
260+
OK 25E3B970
261+
OK
212262
```
213263

214264
# Notes and limitations
@@ -232,7 +282,7 @@ This section will explain some important technical details about the code, and l
232282
* Since PicoLisp is not _event-based_, each new TCP connection spawns a new process, which limits concurrency to the host's available resources.
233283
* Not all [Redis commands](https://redis.io/commands) are implemented, because I didn't have an immediate need for them. There are plans to slowly add new commands as the need arises.
234284
* Using the `client.l` on the command-line, all values are stored as strings. Please use the TCP socket or named pipe directly to store integers and lists.
235-
* Unlike _Redis_, there is no on-disk persistence and **all keys will be lost** when the server is restarted. This library was originally designed to be used as a temporary FIFO queue, with no need to persist the data. Support for persistence can be added eventually, and I'm open to pull-requests.
285+
* ~~Unlike _Redis_, there is no on-disk persistence and **all keys will be lost** when the server is restarted. This library was originally designed to be used as a temporary FIFO queue, with no need to persist the data. Support for persistence can be added eventually, and I'm open to pull-requests.~~ Support for persistence has been added, see [Persistence](#persistence) below.
236286

237287
# How it works
238288

@@ -272,6 +322,81 @@ The forked child processes will each create their own named pipe, called `pipe_c
272322

273323
The idea is to have the **sibling** be the holder of all the **keys**. Every _"Redis-like"_ command will have their data and statistics stored in the memory of the **sibling** process, and the **sibling** will handle receiving and sending its memory contents (keys/values) through named pipes to the respective **child** processes.
274324

325+
# Persistence
326+
327+
Similar to [Redis](https://redis.io/topics/persistence), this database implements "snapshotting" (full memory dump to disk) and "AOF" (append-only log file), however both features are tightly coupled, which makes for a much better experience.
328+
329+
* Persistence is disabled by default, but can be enabled with the `--persist N` parameter, where `N` is the number of seconds between each `BGSAVE` (background save to disk).
330+
* The database is stored in plaintext by default, but can be stored in binary with the `--binary` parameter. Binary format (PLIO) loads and saves _much_ quicker than plaintext, but it becomes difficult to debug a corrupt entry.
331+
* The AOF follows the _WAL_ approach, where each write command is first written to the AOF on disk, and then processed in the key/value memory store.
332+
* The AOF only stores log entries since the previous `SAVE` or `BGSAVE`, so it technically shouldn't grow too large or unmanageable.
333+
* The database snapshot on disk is the most complete and important data, and should be backed up regularly.
334+
* _fsync_ is not managed by the database, so the server admin must ensure AOF log writes are actually persisted to disk.
335+
* The AOF on-disk format is **always plaintext**, to allow easy debugging and repair of a corrupt entry.
336+
* The AOF is opened for writing when the server is started, and closed only when the server is stopped (similar to web server log files). This lowers overhead of appending to the log, but requires care to avoid altering it while the server is running.
337+
* The `SAVE` and `BGSAVE` commands can still be sent even if persistence is disabled. This will dump the in-memory data to disk as if persistence was enabled.
338+
339+
## How persistence is implemented
340+
341+
Here we'll assume persistence was previously enabled and data has already been written and saved to disk.
342+
343+
1. On server start, some memory is pre-allocated according to the DB's file size.
344+
2. The DB is then fully restored to memory
345+
3. If the AOF contains some entries, it is fully replayed to memory
346+
4. The DB is saved once more to disk and the AOF gets wiped
347+
5. A timer is started to perform periodic background DB saves
348+
6. The AOF is opened for writes, and every new client connection sends the command to the AOF
349+
7. When a `BGSAVE` (non-blocking) command is received, a temporay copy of the AOF is made, the current AOF is wiped, and a background process is forked to save the DB to disk
350+
8. When a `SAVE` (blocking) command is received, a the in-memory DB is saved to disk and the AOF is wiped.
351+
9. A backup of the DB file is always made before overwriting the current DB file.
352+
10. To help handle concurrency and persistence, temporary files are named `.kv.db.lock`, `.kv.db.tmp`, `.kv.aof.lock`, and `.kv.aof.tmp`. It's best not to modify or delete those files while the server is running. They can be safely removed while the server is stopped.
353+
354+
## AOF format
355+
356+
The AOF is stored by default in the `kv.aof` file as defined by `*KV_aof`.
357+
358+
Here are two separate entries in a typical AOF:
359+
360+
```
361+
("1596099036.281142829" 54042 ("RPUSH" "mytestlist" ("four" "five" "six")))
362+
("1596099059.683596840" 57240 ("RPUSH" "yourtestlist" ("seven" "eight" "nine")))
363+
```
364+
365+
Each line is a PicoLisp list with only 3 columns:
366+
367+
* Column 1: `String` Unix timestamp with nanoseconds for when the entry was created
368+
* Column 2: `Integer` Non-cryptographically secure hash (CRC) of the command and its arguments
369+
* Column 3: `List` Command name, first argument, and subsequent arguments
370+
371+
When replaying the AOF, the server will ensure the hash of command and arguments match, to guarantee the data is intact. Replaying an AOF can be slow, depending on the number of keys/values.
372+
373+
> **Note:** Manually modifying the AOF will require recomputing and replacing the hash with the result from `(kv-hash)` or PicoLisp `(hash)`.
374+
375+
```
376+
(hash '("RPUSH" "mytestlist" ("four" "five" "zero")))
377+
-> 61453
378+
```
379+
380+
## DB format
381+
382+
The DB is stored by default in the `kv.db` file as defined by `*KV_db`. When backed up, it is named `.kv.db.old`.
383+
384+
Here are two separate entries in a typical DB:
385+
386+
```
387+
("smalldata" ("test1" "test2" "test3" "test4" "test5" "test6"))
388+
("fooh_1000" "test data 1000")
389+
```
390+
391+
Each line is a PicoLisp list with the key in the `(car)`, and values in the `(cadr)`. They are quickly replayed and stored in memory with a simple `(set)` command.
392+
393+
## Differences from Redis
394+
395+
* Unlike _Redis_, persistence only allows specifying a time interval between each `BGSAVE`. Since the AOF is **always enabled**, it's not necessary to "save after N changes", so the config is much simpler.
396+
* Log rewriting is not something that "must be done", because chances are the AOF will never grow too large. Of course that depends on the number of changes occurring between each `BGSAVE`, but even then the AOF is wiped when a `BGSAVE` is initiated (and restored/rewritten if the DB happened to be locked).
397+
* The DB snapshot is used to reconstruct the dataset in memory, not the AOF. The AOF is only used to replay the commands since the last DB save, which is much faster and more efficient, particularly when using `--binary`.
398+
* There is no danger of _losing data_ when switching from `RDB` to `AOF`, because such a concept doesn't even exist.
399+
275400
# Testing
276401

277402
This library comes with a large suite of [unit and integration tests](https://github.com/aw/picolisp-unit). To run the tests, type:

0 commit comments

Comments
 (0)