├── logo.png ├── .gitignore ├── .github └── workflows │ └── tests.yaml ├── test_g.py ├── setup.py ├── README.md ├── greenquery.py ├── tests.py └── greendb.py /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/greendb/HEAD/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/ 2 | config.json 3 | MANIFEST 4 | build 5 | dist 6 | *.pyc 7 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push] 3 | jobs: 4 | tests: 5 | name: ${{ matrix.python-version }} 6 | runs-on: ubuntu-latest 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | python-version: [3.7, "3.10", "3.11"] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-python@v2 14 | with: 15 | python-version: ${{ matrix.python-version }} 16 | - name: pip deps 17 | run: pip install gevent lmdb msgpack-python 18 | - name: runtests 19 | run: python tests.py 20 | -------------------------------------------------------------------------------- /test_g.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test simpledb with many concurrent connections. 3 | """ 4 | from gevent import monkey; monkey.patch_all() 5 | 6 | import time 7 | 8 | import gevent 9 | from greendb import Client 10 | 11 | client = Client() 12 | 13 | def get_sleep_set(k, v, n=1): 14 | client.set(k, v) 15 | #time.sleep(n) 16 | client._sleep(n) 17 | assert client.get(k) == v 18 | data = {b'%s-%032d' % (k, i): 'v%01024d' % i for i in range(100)} 19 | client.mset(data) 20 | resp = client.mget([b'%s-%032d' % (k, i) for i in range(100)]) 21 | assert resp == data 22 | # client.close() # If this were a real app, we would put this here. 23 | 24 | n = 1 25 | t = 100 26 | start = time.time() 27 | 28 | greenlets = [] 29 | for i in range(t): 30 | greenlets.append(gevent.spawn(get_sleep_set, b'k%d' % i, b'v%d' % i, n)) 31 | 32 | for g in greenlets: 33 | g.join() 34 | 35 | client.flush() 36 | stop = time.time() 37 | print('done. slept=%s, took %.2f for %s threads.' % (n, stop - start, t)) 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | 5 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as fh: 6 | readme = fh.read() 7 | 8 | try: 9 | version = __import__('greendb').__version__ 10 | except ImportError: 11 | version = '0.2.3' 12 | 13 | 14 | setup( 15 | name='greendb', 16 | version=version, 17 | description='greendb', 18 | long_description='greendb', 19 | author='Charles Leifer', 20 | author_email='coleifer@gmail.com', 21 | url='http://github.com/coleifer/greendb/', 22 | packages=[], 23 | py_modules=['greendb', 'greenquery'], 24 | install_requires=['lmdb', 'gevent', 'msgpack-python'], 25 | classifiers=[ 26 | 'Development Status :: 4 - Beta', 27 | 'Environment :: Web Environment', 28 | 'Intended Audience :: Developers', 29 | 'License :: OSI Approved :: MIT License', 30 | 'Operating System :: OS Independent', 31 | 'Programming Language :: Python', 32 | ], 33 | scripts=['greendb.py'], 34 | test_suite='tests') 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | ### greendb 4 | 5 | async server frontend for symas lmdb. 6 | 7 | #### description 8 | 9 | greendb is a lightweight server (and Python client) for symas lmdb. The server 10 | uses the new Redis RESPv3 protocol. 11 | 12 | greendb supports multiple independent databases, much like Redis. Values are 13 | serialized using `msgpack`, so the server is capable of storing all the 14 | data-types supported by msgpack. greendb also provides per-database 15 | configuration of multi-value support, allowing you to efficiently store 16 | multiple values at a given key, in sorted order (e.g. for secondary indexes). 17 | 18 | With greendb, database keys are always bytestrings. Values may be: 19 | 20 | * dict 21 | * list 22 | * set 23 | * bytestrings 24 | * unicode strings 25 | * integers 26 | * floating-point 27 | * boolean 28 | * `None` 29 | 30 | #### installing 31 | 32 | ``` 33 | $ pip install greendb 34 | ``` 35 | 36 | Alternatively, you can install from git: 37 | 38 | ``` 39 | $ git clone https://github.com/coleifer/greendb 40 | $ cd greendb 41 | $ python setup.py install 42 | ``` 43 | 44 | Dependencies: 45 | 46 | * [gevent](http://www.gevent.org/) 47 | * [lmdb](https://github.com/dw/py-lmdb) 48 | * [msgpack-python](https://github.com/msgpack/msgpack-python) 49 | 50 | #### running 51 | 52 | ``` 53 | $ greendb.py -h 54 | 55 | Usage: greendb.py [options] 56 | 57 | Options: 58 | -h, --help show this help message and exit 59 | -c CONFIG, --config=CONFIG 60 | Config file (default="config.json") 61 | -D DATA_DIR, --data-dir=DATA_DIR 62 | Directory to store db environment and data. 63 | -d, --debug Log debug messages. 64 | -e, --errors Log error messages only. 65 | -H HOST, --host=HOST Host to listen on. 66 | -l LOG_FILE, --log-file=LOG_FILE 67 | Log file. 68 | -m MAP_SIZE, --map-size=MAP_SIZE 69 | Maximum size of memory-map used for database. The 70 | default value is 256M and should be increased. Accepts 71 | value in bytes or file-size using "M" or "G" suffix. 72 | --max-clients=MAX_CLIENTS 73 | Maximum number of clients. 74 | -n MAX_DBS, --max-dbs=MAX_DBS 75 | Number of databases in environment. Default=16. 76 | -p PORT, --port=PORT Port to listen on. 77 | -r, --reset Reset database and config. All data will be lost. 78 | -s, --sync Flush system buffers to disk when committing a 79 | transaction. Durable but much slower. 80 | -u DUPSORT, --dupsort=DUPSORT 81 | db index(es) to support dupsort 82 | -M, --no-metasync Flush system buffers to disk only once per 83 | transaction, omit the metadata flush. 84 | -W, --writemap Use a writeable memory map. 85 | -A, --map-async When used with "--writemap" (-W), use asynchronous 86 | flushes to disk. 87 | ``` 88 | 89 | Complete config file example with default values: 90 | 91 | ```javascript 92 | 93 | { 94 | "host": "127.0.0.1", 95 | "port": 31337, 96 | "max_clients": 1024, 97 | "path": "data", // Directory for data storage, default is "data" in CWD. 98 | "map_size": "256M", // Default map size is 256MB. INCREASE THIS! 99 | "read_only": false, // Open the database in read-only mode. 100 | "metasync": true, // Sync metadata changes (recommended). 101 | "sync": false, // Sync all changes (durable, but much slower). 102 | "writemap": false, // Use a writable map (probably safe to do). 103 | "map_async": false, // Asynchronous writable map. 104 | "meminit": true, // Initialize new memory pages to zero. 105 | "max_dbs": 16, // Maximum number of DBs. 106 | "max_spare_txns": 64, 107 | "lock": true, // Lock the database when opening environment. 108 | "dupsort": false // Either a boolean or a list of DB indexes. 109 | } 110 | ``` 111 | 112 | Example custom configuration: 113 | 114 | * 1GB max database size 115 | * dupsort enabled on databases 13, 14 and 15 116 | * data stored in /var/lib/greendb/data 117 | 118 | ```javascript 119 | { 120 | "map_size": "1G", 121 | "dupsort": [13, 14, 15], 122 | "path": "/var/lib/greendb/data/" 123 | } 124 | ``` 125 | 126 | Equivalent configuration using command-line arguments: 127 | 128 | ``` 129 | $ greendb.py -m 1G -u 13 -u 14 -u 15 -D /var/lib/greendb/data/ 130 | ``` 131 | 132 | ### client 133 | 134 | A Python client is included in the `greendb` module. All server commands are 135 | implemented as client methods using the lower-case command name, for example: 136 | 137 | ```python 138 | 139 | from greendb import Client 140 | client = Client(host='10.0.0.3') 141 | 142 | # Execute the ENVINFO command. 143 | print(client.envinfo()) 144 | 145 | # Set multiple key/value pairs, read a key, then delete two keys. 146 | client.mset({'k1': 'v1', 'k2': 'v2'}) # MSET 147 | print(client.get('k1')) # GET 148 | client.mdelete(['k1', 'k2']) # MDELETE 149 | ``` 150 | 151 | Additionally, the `Client` implements much of the Python `dict` interface, such 152 | as item get/set/delete, iteration, length, contains, etc. 153 | 154 | If an error occurs, either due to a malformed command (e.g., missing required 155 | parameters) or for any other reason (e.g., attempting to write to a read-only 156 | database), then a `CommandError` will be raised by the client with a message 157 | indicating what caused the error. 158 | 159 | **A note about connections**: the greendb client will automatically connect the 160 | first time you issue a command to the server. The client maintains its own 161 | thread-safe (and greenlet-safe) connection pool. If you wish to explicitly 162 | connect, the client may be used as a context manager. 163 | 164 | In a multi-threaded or multi-greenlet application (e.g. a web app), the client 165 | will maintain a separate connection for each thread/greenlet. 166 | 167 | ### command reference 168 | 169 | Below is the list of supported commands. **Commands are available on the client 170 | using the lower-case command name as the method name**. 171 | 172 |
| command | 175 |description | 176 |arguments | 177 |return value | 178 | 179 |
|---|---|---|---|
| ENVINFO | 181 |metadata and storage configuration settings | 182 |(none) | 183 |dict | 184 |
| ENVSTAT | 187 |metadata related to the global b-tree | 188 |(none) | 189 |dict | 190 |
| FLUSH | 193 |delete all records in the currently-selected database | 194 |(none) | 195 |boolean indicating success | 196 |
| FLUSHALL | 199 |delete all records in all databases | 200 |(none) | 201 |dict mapping database index to boolean | 202 |
| PING | 205 |ping the server | 206 |(none) | 207 |"pong" | 208 |
| STAT | 211 |metadata related to the currently-selected database b-tree | 212 |(none) | 213 |dict | 214 |
| SYNC | 217 |synchronize database to disk (use when sync=False) | 218 |(none) | 219 |(none) | 220 |
| USE | 223 |select the given database | 224 |database index, 0 through (max_dbs - 1) | 225 |int: active database index | 226 |
| KV commands | 229 ||||
| COUNT | 232 |get the number of key/value pairs in active database | 233 |(none) | 234 |int | 235 |
| DECR | 238 |decrement the value at the given key | 239 |amount to decrement by (optional, default is 1) | 240 |int or float | 241 |
| INCR | 244 |increment the value at the given key | 245 |amount to increment by (optional, default is 1) | 246 |int or float | 247 |
| CAS | 250 |compare-and-set | 251 |key, original value, new value | 252 |boolean indicating success or failure | 253 |
| DELETE | 256 |delete a key and any value(s) associated | 257 |key | 258 |int: number of keys removed (1 on success, 0 if key not found) | 259 |
| DELETEDUP | 262 |delete a particular key/value pair when dupsort is enabled | 263 |key, value to delete | 264 |int: number of key+value removed (1 on success, 0 if key+value not found) | 265 |
| DELETEDUPRAW | 268 |delete a particular key/value pair when dupsort is enabled using an 269 | unserialized bytestring as the value | 270 |key, value to delete | 271 |int: number of key+value removed (1 on success, 0 if key+value not found) | 272 |
| DUPCOUNT | 275 |get number of values stored at the given key (requires dupsort) | 276 |key | 277 |int: number of values, or None if key does not exist | 278 |
| EXISTS | 281 |determine if the given key exists | 282 |key | 283 |bool | 284 |
| GET | 287 |get the value associated with a given key. If dupsort is enabled and 288 | multiple values are present, the one that is sorted first will be returned. | 289 |key | 290 |value or None | 291 |
| GETDUP | 294 |get all values associated with a given key (requires dupsort) | 295 |key | 296 |list of values or None if key does not exist | 297 |
| LENGTH | 300 |get the length of the value stored at a given key, e.g. for a string 301 | this returns the number of characters, for a list the number of items, etc. | 302 |key | 303 |length of value or None if key does not exist | 304 |
| POP | 307 |atomically get and delete the value at a given key. If dupsort is 308 | enabled and multiple values are present, the one that is sorted first will 309 | be removed and returned. | 310 |key | 311 |value or None | 312 |
| REPLACE | 315 |atomically get and set the value at a given key. If dupsort is enabled, 316 | the first value will be returned (if exists) and ALL values will be removed 317 | so that only the new value is stored. | 318 |key | 319 |previous value or None | 320 |
| SET | 323 |store a key/value pair. If dupsort is enabled, duplicate values will be 324 | stored at the given key in sorted-order. Additionally, if dupsort is 325 | enabled and the exact key/value pair already exist, no changes are made. | 326 |key, value | 327 |int: 1 if new key/value added, 0 if dupsort is enabled and the 328 | key/value already exist | 329 |
| SETDUP | 332 |store a key/value pair, treating duplicates as successful writes 333 | (requires dupsort). Unlike SET, if the exact key/value pair already exists, 334 | this command will return 1 indicating success. | 335 |key, value | 336 |int: 1 on success | 337 |
| SETDUPRAW | 340 |store a key/value pair, treating duplicates as successful writes 341 | (requires dupsort). Additionally, the value is not serialized, but is 342 | stored as a raw bytestring. | 343 |key, value (bytes) | 344 |int: 1 on success | 345 |
| SETNX | 348 |store a key/value pair only if the key does not exist | 349 |key, value | 350 |int: 1 on success, 0 if key already exists | 351 |
| Bulk KV commands | 354 ||||
| MDELETE | 357 |delete multiple keys | 358 |list of keys | 359 |int: number of keys deleted | 360 |
| MGET | 363 |get the value of multiple keys | 364 |list of keys | 365 |dict of key and value. Keys that were requested, but which do not 366 | exist are not included in the response. | 367 |
| MGETDUP | 370 |get all values of multiple keys (requires dupsort) | 371 |list of keys | 372 |dict of key to list of values. Keys that were requested, but which do 373 | not exist, are not included in the response. | 374 |
| MPOP | 377 |atomically get and delete the value of multiple keys. If dupsort is 378 | enabled and multiple values are stored at a given key, only the first value 379 | will be removed. | 380 |list of keys | 381 |dict of key to value | 382 |
| MREPLACE | 385 |atomically get and set the value of multiple keys. If dupsort is 386 | enabled and multiple values are stored at a given key, only the first value 387 | will be returned and all remaining values discarded. | 388 |dict of key to value | 389 |dict of key to previous value. Keys that did not exist previously will 390 | not be included in the response. | 391 |
| MSET | 394 |set the value of multiple keys. | 395 |dict of key to value | 396 |int: number of key / value pairs set | 397 |
| MSETDUP | 400 |store multiple key/value pairs, treating duplicates as successful 401 | writes (requires dupsort). Unlike MSET, if the exact key/value pair already 402 | exists, this command will treat the write as a success. | 403 |dict of key to value | 404 |int: number of key / value pairs set | 405 |
| MSETNX | 408 |store multiple key/value pair only if the key does not exist | 409 |dict of key to value | 410 |int: number of key / value pairs set | 411 |
| Cursor / range commands | 414 ||||
| DELETERANGE | 417 |delete a range of keys using optional inclusive start/end-points | 418 |start key (optional), end key (optional), count (optional) | 419 |int: number of keys deleted | 420 |
| GETRANGE | 423 |retrieve a range of key/value pairs using optional inclusive 424 | start/end-points | 425 |start key (optional), end key (optional), count (optional) | 426 |list of [key, value] lists | 427 |
| GETRANGEDUPRAW | 430 |retrieve a range of duplicate values stored in a given key, using 431 | optional (inclusive) start/end-points | 432 |key, start value (optional), end value (optional), count (optional) | 433 |list of values (as bytestrings) | 434 |
| KEYS | 437 |retrieve a range of keys using optional inclusive start/end-points | 438 |start key (optional), end key (optional), count (optional) | 439 |list of keys | 440 |
| PREFIX | 443 |retrieve a range of key/value pairs which match the given prefix | 444 |prefix, count (optional) | 445 |list of [key, value] lists | 446 |
| VALUES | 449 |retrieve a range of values using optional inclusive start/end-points | 450 |start key (optional), end key (optional), count (optional) | 451 |list of values | 452 |
| Client commands | 455 ||||
| QUIT | 458 |disconnect from server | 459 |(none) | 460 |int: 1 | 461 |
| SHUTDOWN | 464 |terminate server process from client (be careful!) | 465 |(none) | 466 |(none) | 467 |