"
description: |
immudb - the tamperproof database
vendor: "Codenotary Inc."
homepage: "https://github.com/codenotary/immudb"
license: "Apache 2"
bindir: "/usr/sbin"
files:
"./bin/immudb": "/usr/sbin/immudb"
"./bin/immugw": "/usr/sbin/immugw"
"./bin/immuclient": "/usr/local/bin/immuclient"
config_files:
./packaging/deb/init.d/immudb: "/etc/init.d/immudb"
./packaging/deb/init.d/immugw: "/etc/init.d/immugw"
./packaging/deb/default/immudb: "/etc/default/immudb"
./packaging/deb/default/immugw: "/etc/default/immugw"
./packaging/deb/default/immudb.toml.dist: "/etc/immudb/immudb.toml"
./packaging/deb/default/immugw.toml.dist: "/etc/immudb/immugw.toml"
./packaging/deb/default/immuclient.toml.dist: "/etc/immudb/immuclient.toml"
./packaging/deb/systemd/immudb.service: "/usr/lib/systemd/system/immudb.service"
./packaging/deb/systemd/immugw.service: "/usr/lib/systemd/system/immugw.service"
./packaging/deb/man/immuclient.1: "/usr/local/share/man/man1"
./packaging/deb/man/immudb.1: "/usr/local/share/man/man1"
./packaging/deb/man/immugw.1: "/usr/local/share/man/man1"
overrides:
rpm:
scripts:
postinstall: ./packaging/rpm/control/postinst
deb:
scripts:
postinstall: ./packaging/deb/control/postinst
================================================
FILE: tools/packaging/deb/control/postinst
================================================
#!/bin/sh
set -e
[ -f /etc/default/immudb ] && . /etc/default/immudb
IS_UPGRADE=false
case "$1" in
configure)
[ -z "$IMMU_USER" ] && IMMU_USER="immu"
[ -z "$IMMU_GROUP" ] && IMMU_GROUP="immu"
if ! getent group "$IMMU_GROUP" > /dev/null 2>&1 ; then
addgroup --system "$IMMU_GROUP" --quiet
fi
if ! id $IMMU_USER > /dev/null 2>&1 ; then
adduser --system --home /usr/share/immudb --no-create-home \
--ingroup "$IMMU_GROUP" --disabled-password --shell /bin/false \
"$IMMU_USER"
fi
# Set user permissions on /var/log/immudb, /var/lib/immudb
mkdir -p /var/log/immudb /var/lib/immudb /etc/immudb /usr/share/immudb
chown -R $IMMU_USER:$IMMU_GROUP /var/log/immudb /var/lib/immudb /usr/share/immudb
chmod 755 /var/log/immudb /var/lib/immudb /usr/share/immudb
chmod +x /usr/sbin/immudb
chmod +x /usr/sbin/immugw
chmod +x /usr/local/bin/immuclient
# rebuild manpages
mandb -q
# configuration files should not be modifiable by immu user, as this can be a security issue
chown -Rh root:$IMMU_GROUP /etc/immudb
chmod 755 /etc/immudb
find /etc/immudb -type f -exec chmod 640 {} ';'
find /etc/immudb -type d -exec chmod 755 {} ';'
# If $1=configure and $2 is set, this is an upgrade
if [ "$2" != "" ]; then
IS_UPGRADE=true
fi
if [ "x$IS_UPGRADE" != "xtrue" ]; then
if command -v systemctl >/dev/null; then
echo "### NOT starting on installation, please execute the following statements to configure immudb to start automatically using systemd"
echo " sudo /bin/systemctl daemon-reload"
echo " sudo /bin/systemctl enable immudb"
echo " sudo /bin/systemctl enable immugw"
echo "### You can start immudb by executing"
echo " sudo /bin/systemctl start immudb"
echo " sudo /bin/systemctl start immugw"
elif command -v update-rc.d >/dev/null; then
echo "### NOT starting immudb by default on bootup, please execute"
echo " sudo update-rc.d immudb defaults 95 10"
echo " sudo update-rc.d immugw defaults 95 10"
echo "### In order to start immudb, execute"
echo " sudo service immudb start"
echo " sudo service immugw start"
fi
elif [ "$RESTART_ON_UPGRADE" = "true" ]; then
echo -n "Restarting immudb services..."
if command -v systemctl >/dev/null; then
systemctl daemon-reload
systemctl restart immudb || true
systemctl restart immugw || true
elif [ -x /etc/init.d/immudb ]; then
if command -v invoke-rc.d >/dev/null; then
invoke-rc.d immudb restart || true
invoke-rc.d immugw restart || true
else
/etc/init.d/immudb restart || true
/etc/init.d/immugw restart || true
fi
fi
echo " OK"
fi
;;
esac
================================================
FILE: tools/packaging/deb/default/immuclient.ini.dist
================================================
address = 127.0.0.1
port = 3322
================================================
FILE: tools/packaging/deb/default/immudb
================================================
IMMU_USER=immu
IMMU_GROUP=immu
IMMU_HOME=/usr/share/immudb
LOG_DIR=/var/log/immudb
DATA_DIR=/var/lib/immudb
MAX_OPEN_FILES=10000
CONF_DIR=/etc/immudb
CONF_FILE=/etc/immudb/immudb.toml
RESTART_ON_UPGRADE=true
# Only used on systemd systems
PID_FILE_DIR=/var/run/immudb.pid
================================================
FILE: tools/packaging/deb/default/immudb.ini.dist
================================================
dir = /var/lib/immudb
network = tcp
address = 0.0.0.0
port = 3322
dbname = immudb
#pidfile = /var/run/immudb.pid
logfile = /var/log/immudb/immudb.log
mtls = false
#pkey = ./tools/mtls/3_application/private/localhost.key.pem
#certificate = ./tools/mtls/3_application/certs/localhost.cert.pem
#clientcas = ./tools/mtls/2_intermediate/certs/ca-chain.cert.pem
================================================
FILE: tools/packaging/deb/default/immugw
================================================
IMMU_USER=immu
IMMU_GROUP=immu
IMMU_HOME=/usr/share/immudb
LOG_DIR=/var/log/immudb
DATA_DIR=/var/lib/immudb
MAX_OPEN_FILES=10000
CONF_DIR=/etc/immudb
CONF_FILE=/etc/immudb/immugw.toml
RESTART_ON_UPGRADE=true
# Only used on systemd systems
PID_FILE_DIR=/var/run/immugw.pid
================================================
FILE: tools/packaging/deb/default/immugw.ini.dist
================================================
address = 0.0.0.0
port = 3323
immudaddress = 127.0.0.1
immudport = 3322
#pidfile = /var/run/immugw.pid
logfile = /var/log/immudb/immugw.log
mtls = false
servername = localhost
#pkey = ./tools/mtls/4_client/private/localhost.key.pem
#certificate = ./tools/mtls/4_client/certs/localhost.cert.pem
#clientcas = ./tools/mtls/2_intermediate/certs/ca-chain.cert.pem
================================================
FILE: tools/packaging/deb/init.d/immudb
================================================
#! /usr/bin/env bash
# chkconfig: 2345 80 05
# description: immudb database
# processname: immudb
# config: /etc/immudb/immudb.toml
# pidfile: /var/run/immudb.pid
### BEGIN INIT INFO
# Provides: immudb
# Required-Start: $all
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start immudb at boot time
### END INIT INFO
# tested on
# 1. New lsb that define start-stop-daemon
# 3. Centos with initscripts package installed
PATH=/bin:/usr/bin:/sbin:/usr/sbin
NAME=immudb
DESC="immud - the tamperproof database"
DEFAULT=/etc/default/$NAME
IMMU_USER=immu
IMMU_GROUP=immu
IMMU_HOME=/usr/share/immudb
CONF_DIR=/etc/immudb
WORK_DIR=$IMMU_HOME
DATA_DIR=/var/lib/immudb
LOG_DIR=/var/log/immudb
CONF_FILE=$CONF_DIR/immudb.toml
MAX_OPEN_FILES=10000
PID_FILE=/var/run/$NAME.pid
DAEMON=/usr/sbin/$NAME
umask 0027
if [ ! -x $DAEMON ]; then
echo "Program not installed or not executable"
exit 5
fi
. /lib/lsb/init-functions
if [ -r /etc/default/rcS ]; then
. /etc/default/rcS
fi
# overwrite settings from default file
if [ -f "$DEFAULT" ]; then
. "$DEFAULT"
fi
DAEMON_OPTS="--config ${CONF_FILE}"
function checkUser() {
if [ `id -u` -ne 0 ]; then
echo "You need root privileges to run this script"
exit 4
fi
}
case "$1" in
start)
checkUser
log_daemon_msg "Starting $DESC"
pid=`pidofproc -p $PID_FILE immud`
if [ -n "$pid" ] ; then
log_begin_msg "Already running."
log_end_msg 0
exit 0
fi
# Prepare environment
mkdir -p "$LOG_DIR" "$DATA_DIR" && chown "$IMMU_USER":"$IMMU_GROUP" "$LOG_DIR" "$DATA_DIR"
touch "$PID_FILE" && chown "$IMMU_USER":"$IMMU_GROUP" "$PID_FILE"
if [ -n "$MAX_OPEN_FILES" ]; then
ulimit -n $MAX_OPEN_FILES
fi
# Start Daemon
start-stop-daemon --start -b --chdir "$WORK_DIR" --user "$IMMU_USER" -c "$IMMU_USER" --pidfile "$PID_FILE" --exec $DAEMON -- $DAEMON_OPTS
return=$?
if [ $return -eq 0 ]
then
sleep 1
# check if pid file has been written to
if ! [[ -s $PID_FILE ]]; then
log_end_msg 1
exit 1
fi
i=0
timeout=10
# Wait for the process to be properly started before exiting
until { cat "$PID_FILE" | xargs kill -0; } >/dev/null 2>&1
do
sleep 1
i=$(($i + 1))
if [ $i -gt $timeout ]; then
log_end_msg 1
exit 1
fi
done
fi
log_end_msg $return
;;
stop)
checkUser
log_daemon_msg "Stopping $DESC"
if [ -f "$PID_FILE" ]; then
start-stop-daemon --stop --pidfile "$PID_FILE" \
--user "$IMMU_USER" \
--retry=TERM/20/KILL/5 >/dev/null
if [ $? -eq 1 ]; then
log_progress_msg "$DESC is not running but pid file exists, cleaning up"
elif [ $? -eq 3 ]; then
PID="`cat $PID_FILE`"
log_failure_msg "Failed to stop $DESC (pid $PID)"
exit 1
fi
rm -f "$PID_FILE"
else
log_progress_msg "(not running)"
fi
log_end_msg 0
;;
status)
status_of_proc -p $PID_FILE immudb immudb && exit 0 || exit $?
;;
restart|force-reload)
if [ -f "$PID_FILE" ]; then
$0 stop
sleep 1
fi
$0 start
;;
*)
log_success_msg "Usage: $0 {start|stop|restart|force-reload|status}"
exit 3
;;
esac
================================================
FILE: tools/packaging/deb/init.d/immugw
================================================
#! /usr/bin/env bash
# chkconfig: 2345 80 05
# description: immugw - immudb API Gateway
# processname: immugw
# config: /etc/immudb/immugw.toml
# pidfile: /var/run/immugw.pid
### BEGIN INIT INFO
# Provides: immugw
# Required-Start: $all
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start immugw at boot time
### END INIT INFO
# tested on
# 1. New lsb that define start-stop-daemon
# 3. Centos with initscripts package installed
PATH=/bin:/usr/bin:/sbin:/usr/sbin
NAME=immugw
DESC="immugw - API Gateway for immud - the tamperproof database"
DEFAULT=/etc/default/$NAME
IMMU_USER=immu
IMMU_GROUP=immu
IMMU_HOME=/usr/share/immudb
CONF_DIR=/etc/immudb
WORK_DIR=$IMMU_HOME
DATA_DIR=/var/lib/immudb
LOG_DIR=/var/log/immudb
CONF_FILE=$CONF_DIR/immugw.toml
MAX_OPEN_FILES=10000
PID_FILE=/var/run/$NAME.pid
DAEMON=/usr/sbin/$NAME
umask 0027
if [ ! -x $DAEMON ]; then
echo "Program not installed or not executable"
exit 5
fi
. /lib/lsb/init-functions
if [ -r /etc/default/rcS ]; then
. /etc/default/rcS
fi
# overwrite settings from default file
if [ -f "$DEFAULT" ]; then
. "$DEFAULT"
fi
DAEMON_OPTS="--config ${CONF_FILE}"
function checkUser() {
if [ `id -u` -ne 0 ]; then
echo "You need root privileges to run this script"
exit 4
fi
}
case "$1" in
start)
checkUser
log_daemon_msg "Starting $DESC"
pid=`pidofproc -p $PID_FILE immugw`
if [ -n "$pid" ] ; then
log_begin_msg "Already running."
log_end_msg 0
exit 0
fi
# Prepare environment
mkdir -p "$LOG_DIR" "$DATA_DIR" && chown "$IMMU_USER":"$IMMU_GROUP" "$LOG_DIR" "$DATA_DIR"
touch "$PID_FILE" && chown "$IMMU_USER":"$IMMU_GROUP" "$PID_FILE"
if [ -n "$MAX_OPEN_FILES" ]; then
ulimit -n $MAX_OPEN_FILES
fi
# Start Daemon
start-stop-daemon --start -b --chdir "$WORK_DIR" --user "$IMMU_USER" -c "$IMMU_USER" --pidfile "$PID_FILE" --exec $DAEMON -- $DAEMON_OPTS
return=$?
if [ $return -eq 0 ]
then
sleep 1
# check if pid file has been written to
if ! [[ -s $PID_FILE ]]; then
log_end_msg 1
exit 1
fi
i=0
timeout=10
# Wait for the process to be properly started before exiting
until { cat "$PID_FILE" | xargs kill -0; } >/dev/null 2>&1
do
sleep 1
i=$(($i + 1))
if [ $i -gt $timeout ]; then
log_end_msg 1
exit 1
fi
done
fi
log_end_msg $return
;;
stop)
checkUser
log_daemon_msg "Stopping $DESC"
if [ -f "$PID_FILE" ]; then
start-stop-daemon --stop --pidfile "$PID_FILE" \
--user "$IMMU_USER" \
--retry=TERM/20/KILL/5 >/dev/null
if [ $? -eq 1 ]; then
log_progress_msg "$DESC is not running but pid file exists, cleaning up"
elif [ $? -eq 3 ]; then
PID="`cat $PID_FILE`"
log_failure_msg "Failed to stop $DESC (pid $PID)"
exit 1
fi
rm -f "$PID_FILE"
else
log_progress_msg "(not running)"
fi
log_end_msg 0
;;
status)
status_of_proc -p $PID_FILE immugw immugw && exit 0 || exit $?
;;
restart|force-reload)
if [ -f "$PID_FILE" ]; then
$0 stop
sleep 1
fi
$0 start
;;
*)
log_success_msg "Usage: $0 {start|stop|restart|force-reload|status}"
exit 3
;;
esac
================================================
FILE: tools/packaging/deb/man/immuclient.1
================================================
.TH "immu" "1" "Apr 2020" "Auto generated by spf13/cobra" ""
.nh
.ad l
.SH NAME
.PP
immu \-
.SH SYNOPSIS
.PP
\fBimmu [flags]\fP
.SH DESCRIPTION
.SH OPTIONS
.PP
\fB\-a\fP, \fB\-\-address\fP="127.0.0.1"
immudb host address
.PP
\fB\-\-config\fP=""
config file (default path are config or $HOME. Default filename is immu.toml)
.PP
\fB\-h\fP, \fB\-\-help\fP[=false]
help for immu
.PP
\fB\-p\fP, \fB\-\-port\fP=3322
immudb port number
.SH SEE ALSO
.PP
\fBimmu\-backup(1)\fP, \fBimmu\-consistency(1)\fP, \fBimmu\-count(1)\fP, \fBimmu\-get(1)\fP, \fBimmu\-history(1)\fP, \fBimmu\-inclusion(1)\fP, \fBimmu\-ping(1)\fP, \fBimmu\-reference(1)\fP, \fBimmu\-restore(1)\fP, \fBimmu\-safeget(1)\fP, \fBimmu\-safereference(1)\fP, \fBimmu\-safeset(1)\fP, \fBimmu\-safezadd(1)\fP, \fBimmu\-scan(1)\fP, \fBimmu\-set(1)\fP, \fBimmu\-zadd(1)\fP, \fBimmu\-zscan(1)\fP
.SH HISTORY
.PP
11\-Apr\-2020 Auto generated by spf13/cobra
================================================
FILE: tools/packaging/deb/man/immudb.1
================================================
.TH "immud" "1" "Apr 2020" "Auto generated by spf13/cobra" ""
.nh
.ad l
.SH NAME
.PP
immud \-
.SH SYNOPSIS
.PP
\fBimmud [flags]\fP
.SH DESCRIPTION
.SH OPTIONS
.PP
\fB\-a\fP, \fB\-\-address\fP="0.0.0.0"
bind address
.PP
\fB\-\-certificate\fP="./tools/mtls/3\_application/certs/localhost.cert.pem"
server certificate file path
.PP
\fB\-\-clientcas\fP="./tools/mtls/2\_intermediate/certs/ca\-chain.cert.pem"
clients certificates list. Aka certificate authority
.PP
\fB\-\-config\fP=""
config file (default path are config or $HOME. Default filename is immud.toml)
.PP
\fB\-n\fP, \fB\-\-dbname\fP="immudb"
db name
.PP
\fB\-d\fP, \fB\-\-dir\fP="."
data folder
.PP
\fB\-h\fP, \fB\-\-help\fP[=false]
help for immud
.PP
\fB\-\-logfile\fP=""
log path with filename. E.g. /tmp/immud/immud.log
.PP
\fB\-m\fP, \fB\-\-mtls\fP[=false]
enable mutual tls
.PP
\fB\-\-pidfile\fP=""
pid path with filename. E.g. /var/run/immud.pid
.PP
\fB\-\-pkey\fP="./tools/mtls/3\_application/private/localhost.key.pem"
server private key path
.PP
\fB\-p\fP, \fB\-\-port\fP=3322
port number
.SH HISTORY
.PP
11\-Apr\-2020 Auto generated by spf13/cobra
================================================
FILE: tools/packaging/deb/man/immugw.1
================================================
.TH "immugw" "1" "Apr 2020" "Auto generated by spf13/cobra" ""
.nh
.ad l
.SH NAME
.PP
immugw \- Immu gateway
.SH SYNOPSIS
.PP
\fBimmugw [flags]\fP
.SH DESCRIPTION
.PP
Immu gateway is an smart proxy for immudb. It exposes all gRPC methods with a rest interface and wrap all SAFE endpoints with a verification service.
.SH OPTIONS
.PP
\fB\-a\fP, \fB\-\-address\fP="0.0.0.0"
immugw host address
.PP
\fB\-\-certificate\fP="./tools/mtls/4\_client/certs/localhost.cert.pem"
server certificate file path
.PP
\fB\-\-clientcas\fP="./tools/mtls/2\_intermediate/certs/ca\-chain.cert.pem"
clients certificates list. Aka certificate authority
.PP
\fB\-\-config\fP=""
config file (default path are config or $HOME. Default filename is immugw.toml)
.PP
\fB\-h\fP, \fB\-\-help\fP[=false]
help for immugw
.PP
\fB\-k\fP, \fB\-\-immudaddress\fP="127.0.0.1"
immudb host address
.PP
\fB\-j\fP, \fB\-\-immudport\fP=3322
immudb port number
.PP
\fB\-\-logfile\fP=""
log path with filename. E.g. /tmp/immugw/immugw.log
.PP
\fB\-m\fP, \fB\-\-mtls\fP[=false]
enable mutual tls
.PP
\fB\-\-pidfile\fP=""
pid path with filename. E.g. /var/run/immugw.pid
.PP
\fB\-\-pkey\fP="./tools/mtls/4\_client/private/localhost.key.pem"
server private key path
.PP
\fB\-p\fP, \fB\-\-port\fP=3323
immugw port number
.PP
\fB\-\-servername\fP="localhost"
used to verify the hostname on the returned certificates
.SH HISTORY
.PP
11\-Apr\-2020 Auto generated by spf13/cobra
================================================
FILE: tools/packaging/deb/systemd/immudb.service
================================================
[Unit]
Description=immudb database daemon
Documentation=https://github.com/codenotary/immudb
Wants=network-online.target
After=network-online.target
[Service]
EnvironmentFile=/etc/default/immudb
User=immu
Group=immu
Type=simple
Restart=on-failure
WorkingDirectory=/usr/share/immudb
RuntimeDirectory=immudb
RuntimeDirectoryMode=0750
ExecStart=/usr/sbin/immudb --config /etc/immudb/immudb.toml
LimitNOFILE=10000
TimeoutStopSec=20
UMask=0027
[Install]
WantedBy=multi-user.target
================================================
FILE: tools/packaging/deb/systemd/immugw.service
================================================
[Unit]
Description=immugw - immudb API gateway
Documentation=https://github.com/codenotary/immudb
Wants=network-online.target
Requires=immudb.service
[Service]
EnvironmentFile=/etc/default/immugw
User=immu
Group=immu
Type=simple
Restart=on-failure
WorkingDirectory=/usr/share/immudb
RuntimeDirectory=immugw
RuntimeDirectoryMode=0750
ExecStartPre=/bin/sleep 30
ExecStart=/usr/sbin/immugw --config /etc/immudb/immugw.toml
LimitNOFILE=10000
TimeoutStopSec=20
UMask=0027
[Install]
WantedBy=multi-user.target
================================================
FILE: tools/rndpass/startup.sh
================================================
#!/bin/sh
if [ -f /etc/sysconfig/immudb ]
then
source /etc/sysconfig/immudb
fi
if [ -z "$IMMUDB_ADMIN_PASSWORD" ]
then
IMMUDB_ADMIN_PASSWORD=`tr -cd '[:alnum:].,:;/@_=' < /dev/urandom|head -c 16`
echo "Generated immudb password: $IMMUDB_ADMIN_PASSWORD"
fi
export IMMUDB_ADMIN_PASSWORD
exec $@
================================================
FILE: tools/testing/stress_tool_sql.go
================================================
/*
Copyright 2025 Codenotary Inc. All rights reserved.
SPDX-License-Identifier: BUSL-1.1
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://mariadb.com/bsl11/
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"flag"
"log"
"math/rand"
"sync"
"time"
"github.com/codenotary/immudb/pkg/api/schema"
immudb "github.com/codenotary/immudb/pkg/client"
"google.golang.org/protobuf/types/known/emptypb"
)
type Entry struct {
id int
value []byte
}
type cfg struct {
IpAddr string
Port int
Username string
Password string
DBName string
committers int
kvCount int
vLen int
rndValues bool
readers int
rdCount int
readDelay int
readPause int
readRenew bool
compactDelay int
compactCycles int
verifiers int
vrCount int
sessionMode bool
transactions bool
}
func parseConfig() (c cfg) {
flag.StringVar(&c.IpAddr, "addr", "", "IP address of immudb server")
flag.IntVar(&c.Port, "port", 3322, "Port number of immudb server")
flag.StringVar(&c.Username, "user", "immudb", "Username for authenticating to immudb")
flag.StringVar(&c.Password, "pass", "immudb", "Password for authenticating to immudb")
flag.StringVar(&c.DBName, "db", "defaultdb", "Name of the database to use")
flag.IntVar(&c.committers, "committers", 10, "number of concurrent committers")
flag.IntVar(&c.kvCount, "kvCount", 1_000, "number of kv entries per tx")
flag.IntVar(&c.vLen, "vLen", 32, "value length (bytes)")
flag.BoolVar(&c.rndValues, "rndValues", true, "values are randomly generated")
flag.IntVar(&c.readers, "readers", 0, "number of concurrent readers")
flag.IntVar(&c.rdCount, "rdCount", 100, "number of reads for each readers")
flag.IntVar(&c.readDelay, "readDelay", 100, "Readers start delay (ms)")
flag.IntVar(&c.readPause, "readPause", 0, "Readers pause at every cycle")
flag.BoolVar(&c.readRenew, "readRenew", false, "renew snapshots on read")
flag.IntVar(&c.compactDelay, "compactDelay", 0, "Milliseconds wait before compactions (0 disable)")
flag.IntVar(&c.compactCycles, "compactCycles", 0, "Number of compaction to perform")
flag.IntVar(&c.verifiers, "verifiers", 0, "number of verifiers readers")
flag.IntVar(&c.vrCount, "vrCount", 100, "number of reads for each verifiers")
flag.BoolVar(&c.sessionMode, "sessionMode", false, "use sessions auth mechanism mode")
flag.BoolVar(&c.transactions, "transactions", false, "use transactions to insert data")
flag.Parse()
return
}
func connect(config cfg) (immudb.ImmuClient, context.Context) {
opts := immudb.DefaultOptions().WithAddress(config.IpAddr).WithPort(config.Port)
ctx := context.Background()
var client immudb.ImmuClient
var err error
if config.sessionMode {
client = immudb.NewClient()
err = client.OpenSession(ctx, []byte(config.Username), []byte(config.Password), config.DBName)
if err != nil {
log.Fatalln("Failed to connect. Reason:", err)
}
} else {
client, err = immudb.NewImmuClient(opts)
if err != nil {
log.Fatalln("Failed to connect. Reason:", err)
}
_, err = client.Login(ctx, []byte(config.Username), []byte(config.Password))
if err != nil {
log.Fatalln("Failed to login. Reason:", err.Error())
}
_, err = client.UseDatabase(ctx, &schema.Database{DatabaseName: config.DBName})
if err != nil {
log.Fatalln("Failed to use the database. Reason:", err)
}
}
return client, ctx
}
func idGenerator(c cfg) chan int {
// incremental id generator
ids := make(chan int, 100)
go func() {
for true {
ids <- int(time.Now().UnixNano())
}
}()
return ids
}
func entriesGenerator(c cfg, ids chan int) chan Entry {
entries := make(chan Entry, 100)
rand.Seed(time.Now().UnixNano())
go func() {
log.Printf("Worker is generating rows...\r\n")
for true {
id := <-ids
v := make([]byte, c.vLen)
if c.rndValues {
rand.Read(v)
} else {
copy(v, []byte("mariposa"))
}
entries <- Entry{id: id, value: v}
}
}()
return entries
}
func committer(ctx context.Context, client immudb.ImmuClient, c cfg, entries chan Entry, cid int, wg *sync.WaitGroup) {
log.Printf("Committer %d is inserting data...\r\n", cid)
for i := 0; i < c.kvCount; i++ {
entry := <-entries
_, err := client.SQLExec(ctx, "INSERT INTO entries (id, value, ts) VALUES (@id, @value, now());",
map[string]interface{}{"id": entry.id, "value": entry.value})
if err != nil {
log.Fatalf("Committer %d: Error while inserting value %d [%d]: %s", cid, entry.id, i, err)
}
}
wg.Done()
log.Printf("Committer %d done...\r\n", cid)
}
func committerWithTxs(ctx context.Context, client immudb.ImmuClient, c cfg, entries chan Entry, cid int, wg *sync.WaitGroup) {
log.Printf("Transactions committer %d is inserting data...\r\n", cid)
tx, err := client.NewTx(ctx)
if err != nil {
log.Fatalf("Transactions committer %d: Error while creating transaction: %s", cid, err)
}
for i := 0; i < c.kvCount; i++ {
entry := <-entries
err = tx.SQLExec(ctx, "INSERT INTO entries (id, value, ts) VALUES (@id, @value, now());",
map[string]interface{}{"id": entry.id, "value": entry.value})
if err != nil {
log.Fatalf("Transactions committer %d: Error while inserting value %d [%d]: %s", cid, entry.id, i, err)
}
}
_, err = tx.Commit(ctx)
if err != nil {
if err.Error() != "tx read conflict" {
log.Fatalf("Transactions committer %d: Error while committing transaction: %s", cid, err)
}
}
wg.Done()
log.Printf("Transactions committer %d done...\r\n", cid)
}
func reader(ctx context.Context, client immudb.ImmuClient, c cfg, id int, wg *sync.WaitGroup) {
if c.readDelay > 0 { // give time to populate db
time.Sleep(time.Duration(c.readDelay) * time.Millisecond)
}
log.Printf("Reader %d is reading data\n", id)
for i := 1; i <= c.rdCount; i++ {
r, err := client.SQLQuery(ctx, "SELECT count() FROM entries where id<=@i;", map[string]interface{}{"i": i}, c.readRenew)
if err != nil {
log.Fatalf("Error querying val %d: %s", i, err.Error())
}
ret := r.Rows[0]
n := ret.Values[0].GetN()
if n != int64(i) {
log.Printf("Reader %d read %d vs %d", id, n, i)
}
if c.readPause > 0 {
time.Sleep(time.Duration(c.readPause) * time.Millisecond)
}
}
wg.Done()
log.Printf("Reader %d out\n", id)
}
func readerWithTxs(ctx context.Context, client immudb.ImmuClient, c cfg, id int, wg *sync.WaitGroup) {
if c.readDelay > 0 { // give time to populate db
time.Sleep(time.Duration(c.readDelay) * time.Millisecond)
}
log.Printf("Transactions reader %d is reading data\n", id)
tx, err := client.NewTx(ctx)
if err != nil {
log.Fatalf("Transactions reader %d: Error while creating transaction: %s", id, err)
}
for i := 1; i <= c.rdCount; i++ {
r, err := tx.SQLQuery(ctx, "SELECT count() FROM entries where id<=@i;", map[string]interface{}{"i": i})
if err != nil {
log.Fatalf("Error querying val %d: %s", i, err.Error())
}
ret := r.Rows[0]
n := ret.Values[0].GetN()
if n != int64(i) {
log.Printf("Transactions reader %d read %d vs %d", id, n, i)
}
if c.readPause > 0 {
time.Sleep(time.Duration(c.readPause) * time.Millisecond)
}
}
_, err = tx.Commit(ctx)
if err != nil {
return
}
wg.Done()
log.Printf("Transactions reader %d out\n", id)
}
func verifier(ctx context.Context, client immudb.ImmuClient, c cfg, id int, wg *sync.WaitGroup) {
if c.readDelay > 0 { // give time to populate db
time.Sleep(time.Duration(c.readDelay) * time.Millisecond)
}
log.Printf("Verifier %d is reading data\n", id)
for i := 0; i < c.vrCount; i++ {
idx := 1 + i*c.verifiers + id
r, err := client.SQLQuery(ctx, "SELECT id, value, ts FROM entries WHERE id=@i;", map[string]interface{}{"i": idx}, c.readRenew)
if err != nil {
log.Fatalf("Error querying val %d: %s", i, err.Error())
}
if len(r.Rows) > 0 {
row := r.Rows[0]
err = client.VerifyRow(ctx, row, "entries", []*schema.SQLValue{row.Values[0]})
if err != nil {
log.Fatalf("Verification failed: verifier %d, id %d row %+v", id, idx, row)
}
} else {
log.Printf("Verifier %d no results for id %d", id, idx)
}
if c.readPause > 0 {
time.Sleep(time.Duration(c.readPause) * time.Millisecond)
}
}
wg.Done()
log.Printf("Verifier %d out\n", id)
}
func compactor(ctx context.Context, client immudb.ImmuClient, c cfg, wg *sync.WaitGroup) {
for i := 0; i < c.compactCycles; i++ {
time.Sleep(time.Duration(c.compactDelay) * time.Millisecond)
log.Printf("Compaction %d started", i)
client.CompactIndex(ctx, &emptypb.Empty{})
log.Printf("Compaction %d terminated", i)
}
log.Printf("All compaction terminated")
wg.Done()
}
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
c := parseConfig()
log.Println("Connecting...")
client, ctx := connect(c)
log.Printf("Creating tables\r\n")
_, err := client.SQLExec(ctx, "CREATE TABLE IF NOT EXISTS entries (id INTEGER, value BLOB, ts INTEGER, PRIMARY KEY id);", nil)
if err != nil {
panic(err)
}
ids := idGenerator(c)
entries := entriesGenerator(c, ids)
wg := sync.WaitGroup{}
for i := 0; i < c.committers; i++ {
wg.Add(1)
if c.sessionMode && c.transactions {
go committerWithTxs(ctx, client, c, entries, i, &wg)
} else {
go committer(ctx, client, c, entries, i, &wg)
}
}
for i := 0; i < c.readers; i++ {
wg.Add(1)
if c.sessionMode && c.transactions {
go readerWithTxs(ctx, client, c, i, &wg)
} else {
go reader(ctx, client, c, i, &wg)
}
}
for i := 0; i < c.verifiers; i++ {
wg.Add(1)
go verifier(ctx, client, c, i, &wg)
}
if c.compactDelay > 0 {
wg.Add(1)
go compactor(ctx, client, c, &wg)
}
wg.Wait()
log.Printf("All operations done...\r\n")
r, err := client.SQLQuery(ctx, "SELECT count() FROM entries;", map[string]interface{}{}, true)
if err != nil {
panic(err)
}
row := r.Rows[0]
count := row.Values[0].GetN()
log.Printf("- Counted %d entries\n", count)
}
================================================
FILE: tools/testing/stress_tool_test_kv/README.md
================================================
# Stress tool for KV testing
This tool enables parallel stress test of immudb KV using randomized key / value entries.
Randomized keys are a very heavy test for btree where reads / writes are scattered across
whole btree increasing internal btree cache pressure.
By default the test will connect to an immudb running on localhost.
It will run parallel workers, each worker first inserts the data, then it reads keys
checking if the read value is correct.
## Mixed read-write mode
In order to test how database performs when parallel reads and writes are performed,
use the `-mix-read-writes` flag. By doing so, the test starts with first half of workers.
Once those workers finish their writes, they start the reading test and the second
half of workers is spawned in parallel.
## Sample invocation
```sh
# Run full test
go run .
```
```sh
# Run quick test with reduced amount of entries
go run . -total-entries-written 200000 -total-entries-read 20000
```
```sh
# Run quick test with mixed reads and writes
go run . -total-entries-written 200000 -total-entries-read 20000 -mix-read-writes
```
================================================
FILE: tools/testing/stress_tool_test_kv/stress_tool_kv.go
================================================
package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/binary"
"flag"
"fmt"
"log"
"sync"
"time"
mrand "math/rand"
"github.com/codenotary/immudb/pkg/api/schema"
immudb "github.com/codenotary/immudb/pkg/client"
)
var (
dbHostname = flag.String("host", "localhost", "immudb hostname")
dbPort = flag.Int("port", 3322, "immudb port")
dbName = flag.String("dbname", "defaultdb", "immudb database name")
dbUser = flag.String("user", "immudb", "immudb username")
dbPassword = flag.String("password", "immudb", "immudb password")
parallelism = flag.Int("parallelism", 10, "number of parallel jobs")
mixReadWrites = flag.Bool("mix-read-writes", false, "if true, mix read and write workloads")
seed = flag.Int("seed", 0, "test seed")
totalEntries = flag.Int("total-entries-written", 5_000_000, "total number of entries written during the test")
totalReads = flag.Int("total-entries-read", 10_000, "total number of entries read during the test")
randKeyLen = flag.Bool("randomize-key-length", false, "use randomized key lengths")
help = flag.Bool("help", false, "show help")
)
type etaCalc struct {
what string
start time.Time
lastReport time.Time
totalCount int
}
func newEtaCalc(what string, totalCount int) *etaCalc {
now := time.Now()
return &etaCalc{
what: what,
start: now,
lastReport: now,
totalCount: totalCount,
}
}
func (e *etaCalc) progress(progress int) {
if time.Since(e.lastReport) >= time.Second {
timeElapsed := time.Since(e.start)
timeLeft := time.Duration(
float64(timeElapsed) *
(float64(e.totalCount-progress) / float64(progress)),
)
log.Printf("%s: Entries: %d, elapsed: %v, ETA: %v", e.what, progress, timeElapsed, timeLeft)
e.lastReport = time.Now()
}
}
func (e *etaCalc) printTotal() {
log.Printf("%s: Finished in %v", e.what, time.Since(e.start))
}
func testRun(
seed int,
instance int,
entriesCount int,
readsCount int,
performWrites bool,
performReads bool,
) {
ctx := context.Background()
cl := immudb.NewClient().WithOptions(immudb.DefaultOptions().
WithAddress(*dbHostname).
WithPort(*dbPort),
)
err := cl.OpenSession(ctx, []byte(*dbUser), []byte(*dbPassword), *dbName)
if err != nil {
log.Fatal(err)
}
defer cl.CloseSession(ctx)
var seedKey [sha256.Size]byte
var seedVal [sha256.Size]byte
s := sha256.Sum256([]byte(fmt.Sprintf("keyseed_%d_%d", seed, instance)))
copy(seedKey[:], s[:])
s = sha256.Sum256([]byte(fmt.Sprintf("valueseed_%d_%d", seed, instance)))
copy(seedVal[:], s[:])
key := func(i int) []byte {
var b [8]byte
copy(b[:], seedKey[:])
binary.BigEndian.PutUint64(b[:], uint64(i))
k := sha256.Sum256(b[:])
kLen := len(k)
if *randKeyLen {
if k[kLen-1] > 200 {
return bytes.Repeat(k[:], 1000/len(k))
}
kLen = 8
minLen := 3
kLen = int(k[len(k)-1])%(len(k)-minLen) + minLen
}
return k[:kLen]
}
val := func(i int) []byte {
var b [8]byte
copy(b[:], seedVal[:])
binary.BigEndian.PutUint64(b[:], uint64(i))
k := sha256.Sum256(b[:])
return k[:]
}
if performWrites {
tRep := newEtaCalc(fmt.Sprintf("Insertion %d", instance), entriesCount)
batchSize := 1_000
for i := 0; i < entriesCount; i += batchSize {
alreadyAdded := map[string]struct{}{}
kvs := []*schema.KeyValue{}
for j := 0; j < batchSize && i+j < entriesCount; j++ {
k := key(i + j)
ks := string(k)
if _, found := alreadyAdded[ks]; found {
continue
}
alreadyAdded[ks] = struct{}{}
kvs = append(kvs, &schema.KeyValue{
Key: key(i + j),
Value: val(i + j),
})
}
_, err := cl.SetAll(ctx, &schema.SetRequest{
KVs: kvs,
})
if err != nil {
log.Fatal(err)
}
tRep.progress(i)
}
tRep.printTotal()
}
if performReads {
rnd := mrand.New(mrand.NewSource(int64(seed))) // We want predictable rand source
tRep := newEtaCalc("Reading", readsCount)
for i := 0; i < readsCount; i++ {
j := rnd.Intn(entriesCount)
v, err := cl.Get(ctx, key(j))
if err != nil {
log.Fatal("Error while reading back data ", err)
}
if !bytes.Equal(v.Value, val(j)) {
log.Fatalf(
"Invalid value read (%d)\n"+
"key: %x\n"+
"expected: %x\n"+
"read: %x",
j, key(j), val(j), v.Value,
)
}
tRep.progress(i)
}
tRep.printTotal()
}
}
func entriesForInstance(i int) int {
// Instances could be a little bit skewed in the number of entries they process
start := int(*totalEntries * i / *parallelism)
end := int(*totalEntries * (i + 1) / *parallelism)
return end - start
}
func readsForInstance(i int) int {
// Instances could be a little bit skewed in the number of entries they process
start := int(*totalReads * i / *parallelism)
end := int(*totalReads * (i + 1) / *parallelism)
return end - start
}
func main() {
flag.Parse()
if *help {
flag.PrintDefaults()
return
}
wg := sync.WaitGroup{}
group1Size := *parallelism / 2
// First half - run writes, reads will
for i := 0; i < group1Size; i++ {
wg.Add(1)
go func(i int) {
testRun(*seed, i, entriesForInstance(i), readsForInstance(i), true, !*mixReadWrites)
wg.Done()
}(i)
}
// For mixed workload, we have to wait for the writers to finish first, then we'll
// overlap reads from the first group with writes in the second group
if *mixReadWrites {
wg.Wait()
for i := 0; i < group1Size; i++ {
wg.Add(1)
go func(i int) {
testRun(*seed, i, entriesForInstance(i), readsForInstance(i), false, true)
wg.Done()
}(i)
}
}
// Second half of readers / writers
for i := group1Size; i < *parallelism; i++ {
wg.Add(1)
go func(i int) {
testRun(*seed, i, entriesForInstance(i), readsForInstance(i), true, true)
wg.Done()
}(i)
}
wg.Wait()
}
================================================
FILE: tools.go
================================================
//go:build tools
package tools
import (
_ "github.com/golang/protobuf/proto"
_ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway"
_ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger"
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
_ "github.com/mattn/goveralls"
_ "github.com/ory/go-acc"
_ "github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc"
_ "golang.org/x/tools/cmd/cover"
_ "google.golang.org/grpc"
_ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
)
================================================
FILE: webconsole/.gitignore
================================================
dist
================================================
FILE: webconsole/default/missing/index.html
================================================
No webconsole
immudb
immudb was built without web console support.
See here for instructions, or download an official build.
================================================
FILE: webconsole/webconsole.go
================================================
//go:build webconsole
// +build webconsole
package webconsole
import (
"embed"
"github.com/codenotary/immudb/embedded/logger"
"io/fs"
"net/http"
)
//go:embed dist/*
var content embed.FS
func SetupWebconsole(mux *http.ServeMux, l logger.Logger, addr string) error {
fSys, err := fs.Sub(content, "dist")
if err != nil {
return err
}
l.Infof("Webconsole enabled: %s", addr)
mux.Handle("/", http.FileServer(http.FS(fSys)))
return nil
}
================================================
FILE: webconsole/webconsole_default.go
================================================
//go:build !webconsole
// +build !webconsole
package webconsole
import (
"embed"
"io/fs"
"net/http"
"github.com/codenotary/immudb/embedded/logger"
)
//go:embed default/*
var content embed.FS
func SetupWebconsole(mux *http.ServeMux, l logger.Logger, addr string) error {
fSys, err := fs.Sub(content, "default")
if err != nil {
return err
}
l.Infof("webconsole not built-in")
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/missing/", http.StatusTemporaryRedirect)
})
mux.Handle("/missing/", http.FileServer(http.FS(fSys)))
return nil
}
================================================
FILE: webconsole/webconsole_default_test.go
================================================
//go:build !webconsole
// +build !webconsole
package webconsole
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/codenotary/immudb/embedded/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSetupWebconsoleDefault(t *testing.T) {
req, err := http.NewRequest("GET", "/", nil)
require.NoError(t, err)
handler := http.NewServeMux()
err = SetupWebconsole(handler, logger.NewSimpleLogger("webconsole", os.Stderr), "localhost:8080")
require.NoError(t, err)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusTemporaryRedirect, rr.Code)
page, err := ioutil.ReadAll(rr.Body)
require.NoError(t, err)
assert.Contains(t, string(page), "missing")
}
================================================
FILE: webconsole/webconsole_test.go
================================================
//go:build webconsole
// +build webconsole
package webconsole
import (
"github.com/codenotary/immudb/embedded/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"
)
func TestSetupWebconsole(t *testing.T) {
req, err := http.NewRequest("GET", "/", nil)
require.NoError(t, err)
handler := http.NewServeMux()
SetupWebconsole(handler, logger.NewSimpleLogger("webconsole", os.Stderr), "localhost:8080")
require.NoError(t, err)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
page, err := ioutil.ReadAll(rr.Body)
require.NoError(t, err)
assert.Contains(t, string(page), "immudb webconsole")
}