MemCP for Microservices: Difference between revisions

From MemCP
Jump to navigation Jump to search
(Created page with "MemCP is a neat database to implement microservices. Here's a code example for a simple key value store: →‎microservice democase: a simple key value store with prepared statements: (import "../lib/sql-parser.scm") (import "../lib/queryplan.scm") →‎initialize database and prepare sql statements: (createdatabase "keyvalue" true) (eval (parse_sql "keyvalue" "CREATE TABLE IF NOT EXISTS kv(key TEXT, value TEXT, UNIQUE KEY PRIMARY(key))")) (set item_get (par...")
 
(Refresh MemCP documentation: accuracy, operational guidance, performance profile and maintained API reference)
 
(One intermediate revision by the same user not shown)
Line 1: Line 1:
MemCP is a neat database to implement microservices.
<!-- Copyright (C) 2026 Carl-Philip Haensch -->
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
= MemCP for Microservices =


MemCP can combine database storage, SQL-over-HTTP, and custom Scheme handlers in one process. This removes an application-to-database network hop and can simplify small services, but it is an architectural option rather than a latency guarantee.


Here's a code example for a simple key value store:
== Deployment patterns ==
/* microservice democase: a simple key value store with prepared statements */
 
(import "../lib/sql-parser.scm")
{| class="wikitable"
(import "../lib/queryplan.scm")
! Pattern !! Benefit !! Trade-off
|-
/* initialize database and prepare sql statements */
| Ordinary service plus MySQL protocol || Familiar drivers, connection pools, and separation of concerns || Network/serialization hop remains
(createdatabase "keyvalue" true)
|-
(eval (parse_sql "keyvalue" "CREATE TABLE IF NOT EXISTS kv(key TEXT, value TEXT, UNIQUE KEY PRIMARY(key))"))
| Service plus SQL over HTTP || Minimal client dependency and streamable JSONL || General SQL endpoint needs strict network and grant controls
|-
(set item_get (parse_sql "keyvalue" "SELECT value FROM kv WHERE key = @key"))
| Embedded Scheme handler || Prepared plans and storage access in one process; custom JSON or WebSocket API || Application failure and database failure share a process and deployment lifecycle
(set item_set (parse_sql "keyvalue" "INSERT INTO kv(key, value) VALUES (@key, @value) ON DUPLICATE KEY UPDATE value = @value"))
|}
/*(set item_list (parse_sql "keyvalue" "SELECT key, value FROM kv"))*/
 
Good candidates include small internal APIs, real-time dashboards, local/edge services, event lookup, and low-latency state whose data model remains relational. A large product with independent teams, many language-specific libraries, complex application logic, or strict process-isolation requirements may be easier to operate with a conventional separate service.
 
(define http_handler (begin
== Preparing work outside the request path ==
        (lambda (req res) (begin
 
                (set session (newsession))
The repository's <code>apps/keyvalue.scm</code> example creates a table and parses fixed SELECT/upsert statements once during startup. Each request creates a session, binds the key/value, and evaluates the prepared formula. That avoids parsing and planning the same statement on every request while keeping caller values separate from SQL text.
                (session "key" (req "path"))
 
                (if (equal? (req "method") "GET") (begin
<pre>
                        /* GET = load */
(set item_get (parse_sql "keyvalue"
                        (set resultrow (lambda (resultset) ((res "print") (resultset "value"))))
"SELECT value FROM kv WHERE key = @key"))
                        (eval item_get)
(set item_set (parse_sql "keyvalue"
                ) (begin
"INSERT INTO kv(key, value) VALUES (@key, @value)
                        /* PUT / POST: store */
ON DUPLICATE KEY UPDATE value = @value"))
                        (session "value" ((req "body")))
</pre>
                        (eval item_set)
 
                        ((res "print") "ok")
The complete example installs a request handler and starts its own HTTP listener:
                ))
 
        ))
<pre>
))
(createdatabase "keyvalue" true)
(eval (parse_sql "keyvalue"
(set port 1266)
"CREATE TABLE IF NOT EXISTS kv(key TEXT, value TEXT, UNIQUE KEY PRIMARY(key))"))
(serve port (lambda (req res) (http_handler req res)))
 
The example can be found in <code>apps/keyvalue.scm</code> and can be run with:
(set item_get (parse_sql "keyvalue" "SELECT value FROM kv WHERE key = @key"))
./memcp apps/keyvalue.scm
(set item_set (parse_sql "keyvalue"
Here's a benchmark of a mini-demo on a AMD Ryzen 9 7900X3D 12-Core Processor using <code>ab</code>benchmark tool:
"INSERT INTO kv(key,value) VALUES (@key,@value)
Server Software:       
ON DUPLICATE KEY UPDATE value=@value"))
Server Hostname:        localhost
 
Server Port:           1266
(define http_handler (lambda (req res) (begin
(set session (newsession))
Document Path:          /hi
(session "key" (req "path"))
Document Length:        5 bytes
(if (equal? (req "method") "GET")
(begin
Concurrency Level:      10
(set resultrow (lambda (row) ((res "print") (row "value"))))
Time taken for tests:  42.083 seconds
(eval item_get))
Complete requests:      1000000
(begin
Failed requests:        0
(session "value" ((req "body")))
Total transferred:      106000000 bytes
(eval item_set)
HTML transferred:      5000000 bytes
((res "print") "ok"))))))
Requests per second:    23762.78 [#/sec] (mean)
 
Time per request:      0.421 [ms] (mean)
(serve 1266 (lambda (req res) (http_handler req res)))
Time per request:      0.042 [ms] (mean, across all concurrent requests)
</pre>
Transfer rate:          2459.82 [Kbytes/sec] received
 
Run the maintained repository version with <code>./memcp apps/keyvalue.scm</code>, store a value using <code>curl --data-binary value http://localhost:1266/key</code>, and retrieve it with <code>curl http://localhost:1266/key</code>. This demonstration intentionally omits production authentication and limits; do not expose it unchanged.
Connection Times (ms)
 
              min  mean[+/-sd] median  max
Use SQL constraints and one-statement atomic patterns even inside an embedded handler. In-process access removes transport overhead; it does not remove concurrent requests, transaction conflicts, authorization, or durability decisions.
Connect:        0    0  0.0      0      1
 
Processing:    0    0  0.1      0      1
== Historical HTTP microbenchmark ==
Waiting:        0    0  0.1      0      1
 
Total:          0    0  0.1      0      2
An early AMD Ryzen 9 7900X3D run used ApacheBench against a tiny embedded endpoint with concurrency 10. It reported 1,000,000 completed requests, zero failures, 42.083 seconds, 23,762.78 requests/s, 0.421 ms mean request latency (0.042 ms across concurrent requests), p95 1 ms and maximum 2 ms. The author also observed that <code>ab</code> saturated one client core while MemCP used roughly 20% across other cores.
 
Percentage of the requests served within a certain time (ms)
The original record does not include the exact command, commit, response validation beyond ApacheBench's failure count, server build or repeat runs. It demonstrates that the embedded path can be very small, but it is not a database-query or current-release throughput guarantee. A new comparison must include the handler code, successful body validation, client/server CPU profiles and several raw samples.
  50%      0
 
  66%      0
== Production checklist ==
  75%      0
 
  80%      0
Choose each table ENGINE according to data-loss tolerance, configure total and persistent RAM budgets, and provide health checks, backups, monitoring, and restart validation. Set a strong root password before exposing either the HTTP or MySQL port. Persistent data should use <code>safe</code> unless the documented risks of another engine are explicitly acceptable.
  90%      0
 
  95%      1
Handler latency depends on query shape, compilation and plan-cache state, concurrency, memory pressure, and storage reloads. Benchmark the full authenticated request path with successful-response validation instead of quoting a fixed sub-millisecond figure.
  98%      1
 
  99%      1
Run embedded services under a supervisor, use <code>--no-repl</code>, implement external liveness/readiness checks, limit bodies and queues, propagate cancellation, and test restart/restore. Scaling the process horizontally requires an explicit ownership, replication, or routing design; the current local shard engine is not an automatic multi-node database.
  100%      2 (longest request)
 
It is noteworthy that <code>ab</code>even with a concurrency level of 10 took 100% CPU load on a single core while <code>memcp</code> took a cool portion of 20% of all other cores. So the benchmark tells more about <code>ab</code>s performance than that of MemCP.
See [[In-Database WebApps and REST Services]], [[Persistency and Performance Guarantees]], [[Memory Management and Eviction]], [[Security and Authentication]], and [[Performance Measurement]].

Latest revision as of 12:14, 28 August 2026

MemCP for Microservices

MemCP can combine database storage, SQL-over-HTTP, and custom Scheme handlers in one process. This removes an application-to-database network hop and can simplify small services, but it is an architectural option rather than a latency guarantee.

Deployment patterns

Pattern Benefit Trade-off
Ordinary service plus MySQL protocol Familiar drivers, connection pools, and separation of concerns Network/serialization hop remains
Service plus SQL over HTTP Minimal client dependency and streamable JSONL General SQL endpoint needs strict network and grant controls
Embedded Scheme handler Prepared plans and storage access in one process; custom JSON or WebSocket API Application failure and database failure share a process and deployment lifecycle

Good candidates include small internal APIs, real-time dashboards, local/edge services, event lookup, and low-latency state whose data model remains relational. A large product with independent teams, many language-specific libraries, complex application logic, or strict process-isolation requirements may be easier to operate with a conventional separate service.

Preparing work outside the request path

The repository's apps/keyvalue.scm example creates a table and parses fixed SELECT/upsert statements once during startup. Each request creates a session, binds the key/value, and evaluates the prepared formula. That avoids parsing and planning the same statement on every request while keeping caller values separate from SQL text.

(set item_get (parse_sql "keyvalue"
	"SELECT value FROM kv WHERE key = @key"))
(set item_set (parse_sql "keyvalue"
	"INSERT INTO kv(key, value) VALUES (@key, @value)
	 ON DUPLICATE KEY UPDATE value = @value"))

The complete example installs a request handler and starts its own HTTP listener:

(createdatabase "keyvalue" true)
(eval (parse_sql "keyvalue"
	"CREATE TABLE IF NOT EXISTS kv(key TEXT, value TEXT, UNIQUE KEY PRIMARY(key))"))

(set item_get (parse_sql "keyvalue" "SELECT value FROM kv WHERE key = @key"))
(set item_set (parse_sql "keyvalue"
	"INSERT INTO kv(key,value) VALUES (@key,@value)
	 ON DUPLICATE KEY UPDATE value=@value"))

(define http_handler (lambda (req res) (begin
	(set session (newsession))
	(session "key" (req "path"))
	(if (equal? (req "method") "GET")
		(begin
			(set resultrow (lambda (row) ((res "print") (row "value"))))
			(eval item_get))
		(begin
			(session "value" ((req "body")))
			(eval item_set)
			((res "print") "ok"))))))

(serve 1266 (lambda (req res) (http_handler req res)))

Run the maintained repository version with ./memcp apps/keyvalue.scm, store a value using curl --data-binary value http://localhost:1266/key, and retrieve it with curl http://localhost:1266/key. This demonstration intentionally omits production authentication and limits; do not expose it unchanged.

Use SQL constraints and one-statement atomic patterns even inside an embedded handler. In-process access removes transport overhead; it does not remove concurrent requests, transaction conflicts, authorization, or durability decisions.

Historical HTTP microbenchmark

An early AMD Ryzen 9 7900X3D run used ApacheBench against a tiny embedded endpoint with concurrency 10. It reported 1,000,000 completed requests, zero failures, 42.083 seconds, 23,762.78 requests/s, 0.421 ms mean request latency (0.042 ms across concurrent requests), p95 1 ms and maximum 2 ms. The author also observed that ab saturated one client core while MemCP used roughly 20% across other cores.

The original record does not include the exact command, commit, response validation beyond ApacheBench's failure count, server build or repeat runs. It demonstrates that the embedded path can be very small, but it is not a database-query or current-release throughput guarantee. A new comparison must include the handler code, successful body validation, client/server CPU profiles and several raw samples.

Production checklist

Choose each table ENGINE according to data-loss tolerance, configure total and persistent RAM budgets, and provide health checks, backups, monitoring, and restart validation. Set a strong root password before exposing either the HTTP or MySQL port. Persistent data should use safe unless the documented risks of another engine are explicitly acceptable.

Handler latency depends on query shape, compilation and plan-cache state, concurrency, memory pressure, and storage reloads. Benchmark the full authenticated request path with successful-response validation instead of quoting a fixed sub-millisecond figure.

Run embedded services under a supervisor, use --no-repl, implement external liveness/readiness checks, limit bodies and queues, propagate cancellation, and test restart/restore. Scaling the process horizontally requires an explicit ownership, replication, or routing design; the current local shard engine is not an automatic multi-node database.

See In-Database WebApps and REST Services, Persistency and Performance Guarantees, Memory Management and Eviction, Security and Authentication, and Performance Measurement.