You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* 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
# Redis-inspired key/value store written in PicoLisp
2
2
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.
4
4
5
5
The included `server.l` and `client.l` can be used to send and receive _"Redis-like"_ commands over TCP or UNIX named pipess.
6
6
@@ -11,14 +11,15 @@ The included `server.l` and `client.l` can be used to send and receive _"Redis-l
11
11
3.[Usage](#usage)
12
12
4.[Note and Limitations](#notes-and-limitations)
13
13
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)
18
19
19
20
# Requirements
20
21
21
-
* PicoLisp 32-bit/64-bit `v17.12` to `v20.5.26`
22
+
* PicoLisp 32-bit/64-bit `v17.12` to `v20.6.29`
22
23
* Linux or UNIX-like OS (with support for named pipes)
23
24
24
25
# Getting Started
@@ -47,8 +48,7 @@ That should return some interesting info about your server. See below for more e
47
48
1. Load the client library in your project: `(load "libkvclient.l")`
48
49
2. Set the server password `(setq *KV_pass "yourpass")`
49
50
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"))`
52
52
53
53
Received data will be returned as-is (list, integer, string, etc). Wrap the result like: `(kv-print Result)` to send the output to `STDOUT`:
54
54
@@ -59,8 +59,6 @@ Received data will be returned as-is (list, integer, string, etc). Wrap the resu
59
59
-> "yourpass"
60
60
: (kv-start-client)
61
61
-> T
62
-
: (kv-identify "key1" "value2" "key2" "value3")
63
-
-> "OK 35F2F81D"
64
62
: (kv-send-data '("set" "mykey" 12345))
65
63
-> "OK"
66
64
: (kv-send-data '("get" "mykey"))
@@ -81,18 +79,20 @@ This section describes usage information for the CLI tools `server.l` and `clien
81
79
82
80
## Server
83
81
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:
# 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
212
262
```
213
263
214
264
# Notes and limitations
@@ -232,7 +282,7 @@ This section will explain some important technical details about the code, and l
232
282
* Since PicoLisp is not _event-based_, each new TCP connection spawns a new process, which limits concurrency to the host's available resources.
233
283
* 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.
234
284
* 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.
236
286
237
287
# How it works
238
288
@@ -272,6 +322,81 @@ The forked child processes will each create their own named pipe, called `pipe_c
272
322
273
323
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.
274
324
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`.
* 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)`.
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
+
275
400
# Testing
276
401
277
402
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