diff --git a/Makefile.conf.template b/Makefile.conf.template index c6396cc5f49..0aa9a27cfb2 100644 --- a/Makefile.conf.template +++ b/Makefile.conf.template @@ -18,6 +18,7 @@ #db_oracle= Provides Oracle connectivity for OpenSIPS. | Development library of OCI, typically instantclient-sdk-10.2.0.3 #db_perlvdb= Provides a virtualization framework for OpenSIPS's database access. | Perl library development files, typically libperl-dev #db_postgres= Provides Postgres connectivity for OpenSIPS | PostgreSQL library and development library - typically libpq5 and libpq-dev +#db_redis= Provides Redis (single instance or cluster) row storage for OpenSIPS | Redis client library, hiredis #db_sqlite= Provides SQLite connectivity for OpenSIPS | SQLite library and development library - typically libsqlite3 and libsqlite3-dev #db_unixodbc= Allows to use the unixodbc package with OpenSIPS | ODBC library and ODBC development library #dialplan= Implements generic string translations based on matching and replacement rules | PCRE development library, typically libpcre2-dev @@ -76,7 +77,7 @@ #uuid= UUID generator | uuid-dev # Modules omitted from the default build, generally due to external dependencies. -exclude_modules?= aaa_diameter aaa_radius auth_jwt auth_web3 b2b_logic_xml cachedb_cassandra cachedb_couchbase cachedb_dynamodb cachedb_memcached cachedb_mongodb cachedb_redis carrierroute cgrates compression cpl_c db_berkeley db_http db_mysql db_oracle db_perlvdb db_postgres db_sqlite db_unixodbc dialplan emergency event_rabbitmq event_kafka event_sqs h350 httpd http2d identity jabber json launch_darkly ldap lua mi_xmlrpc mmgeoip opentelemetry osp perl pi_http presence presence_dialoginfo presence_mwi presence_reginfo presence_xml presence_dfks proto_ipsec proto_sctp proto_tls proto_wss pua pua_bla pua_dialoginfo pua_mi pua_reginfo pua_usrloc pua_xmpp python regex rabbitmq_consumer rest_client rls rtp.io siprec sngtc snmpstats stir_shaken tls_mgm tls_openssl tls_wolfssl uuid xcap xcap_client xml xmpp +exclude_modules?= aaa_diameter aaa_radius auth_jwt auth_web3 b2b_logic_xml cachedb_cassandra cachedb_couchbase cachedb_dynamodb cachedb_memcached cachedb_mongodb cachedb_redis carrierroute cgrates compression cpl_c db_berkeley db_http db_mysql db_oracle db_perlvdb db_postgres db_redis db_sqlite db_unixodbc dialplan emergency event_rabbitmq event_kafka event_sqs h350 httpd http2d identity jabber json launch_darkly ldap lua mi_xmlrpc mmgeoip opentelemetry osp perl pi_http presence presence_dialoginfo presence_mwi presence_reginfo presence_xml presence_dfks proto_ipsec proto_sctp proto_tls proto_wss pua pua_bla pua_dialoginfo pua_mi pua_reginfo pua_usrloc pua_xmpp python regex rabbitmq_consumer rest_client rls rtp.io siprec sngtc snmpstats stir_shaken tls_mgm tls_openssl tls_wolfssl uuid xcap xcap_client xml xmpp # Modules forced into the build, even when also listed in exclude_modules. include_modules?= diff --git a/modules/db_redis/Makefile b/modules/db_redis/Makefile new file mode 100644 index 00000000000..d70cb2e5d0c --- /dev/null +++ b/modules/db_redis/Makefile @@ -0,0 +1,11 @@ +# WARNING: do not run this directly, it should be run by the master Makefile + +include ../../Makefile.defs +auto_gen= +NAME=db_redis.so + +# hiredis client library +DEFS+=-I$(LOCALBASE)/include -I$(SYSBASE)/include +LIBS+=-L$(LOCALBASE)/lib -L$(SYSBASE)/lib -lhiredis + +include ../../Makefile.modules diff --git a/modules/db_redis/README b/modules/db_redis/README new file mode 100644 index 00000000000..0b4617d761d --- /dev/null +++ b/modules/db_redis/README @@ -0,0 +1,348 @@ +db_redis Module + __________________________________________________________ + + Table of Contents + + 1. Admin Guide + + 1.1. Overview + 1.2. Data Model + 1.3. Deployment Modes + + 1.3.1. Single endpoint + 1.3.2. Redis Cluster + + 1.4. Dependencies + + 1.4.1. OpenSIPS Modules + 1.4.2. External Libraries or Applications + + 1.5. Exported Parameters + + 1.5.1. connect_timeout (integer) + 1.5.2. query_timeout (integer) + 1.5.3. scan_count (integer) + 1.5.4. mode (string) + + 1.6. Exported Functions + 1.7. Usage + 1.8. Limitations + 1.9. Installation + + 2. Contributors + + 2.1. By Commit Statistics + 2.2. By Commit Activity + + 3. Documentation + + 3.1. Contributors + + List of Tables + + 2.1. Top contributors by DevScore^(1), authored commits^(2) and + lines added/removed^(3) + + 2.2. Most recently active contributors^(1) to this module + + List of Examples + + 1.1. Set connect_timeout parameter + 1.2. Set query_timeout parameter + 1.3. Set scan_count parameter + 1.4. Set mode parameter + 1.5. Persisting the dialog table in a single Redis server + 1.6. Persisting the dialog table in a Redis Cluster + +Chapter 1. Admin Guide + +1.1. Overview + + This module provides a OpenSIPS DB API backend that stores data + in a Redis server or a Redis Cluster. It implements the + structured database interface used by modules such as dialog, + so that their tables can be persisted in Redis instead of a + traditional SQL database. + + Unlike the cachedb_redis module, which exposes Redis through + the key-value NoSQL interface, db_redis presents Redis as a + relational-style backend behind the standard db_url mechanism. + A module is switched to Redis simply by pointing its db_url at + a redis:// URL; no code change is required in the consuming + module. + + The connection type is auto-detected at startup: the module + works either against a single Redis endpoint (a standalone + server, or the current master reached through an external load + balancer/Sentinel setup) or against a fully-enabled Redis + Cluster, where it discovers the whole topology and keeps a + connection open to every master shard. + +1.2. Data Model + + Redis is schemaless, so the module needs the table layout to be + declared out of band. Both the schema definitions and the data + are stored as Redis hashes: + * Schema - the hash “schema:” describes one table. It + holds a “__cols” field with the ordered, space-separated + list of column names, a “__pk” field naming the primary-key + column, and one field per column giving its type as + “type[,null][,auto]”. The supported types are int, bigint, + double, string, blob and datetime. The null attribute marks + a nullable column and auto marks an auto-increment primary + key. + * Rows - each row is the hash “
:”, + with one field per non-NULL column. A NULL column is + represented by the absence of the field. Values of blob + columns are stored verbatim (binary-safe), with no encoding + overhead. + * Sequences - auto-increment primary keys are generated with + INCR on the counter “seq:
”. + * Table version - the standard OpenSIPS “version” table is + provisioned like any other table, with one row per served + table, for example “HSET version:dialog table_name dialog + table_version 12”. + + The key prefixes “schema:” and “seq:” are reserved by the + module and must not be used as table names. Ready-made + provisioning files are shipped under the “scripts/redis/” + directory; see that directory's README for details. A schema is + loaded from Redis on first access to a table and cached for the + lifetime of the connection. + +1.3. Deployment Modes + + At connection time the module issues an “INFO CLUSTER” command + and inspects the “cluster_enabled” field to decide how to + operate. The detected behaviour can be pinned with the mode + parameter. + +1.3.1. Single endpoint + + Used for a standalone Redis server, or when a Redis Sentinel + deployment is fronted by an external load balancer that always + routes to the current master. The module keeps a single + connection and transparently reconnects on I/O errors or when + it receives a “-READONLY” reply (which indicates it is talking + to a demoted master after a failover). Sentinel discovery and + master election are expected to be handled externally. + +1.3.2. Redis Cluster + + When the endpoint is a cluster node, the module fetches the + full topology (via “CLUSTER SHARDS”, falling back to “CLUSTER + SLOTS” on older servers) starting from the single seed host + given in db_url. It then opens and keeps warm a connection to + every master shard, so that a key routed to any shard can be + served immediately over an already established connection + rather than paying a new TCP handshake on each redirect. + + Each key is routed to its shard by computing the CRC16 hash + slot (honouring “{hash tag}” braces). “MOVED” redirects are + served from the warm connection pool and trigger a deferred + topology refresh; “ASK” redirects are followed with a one-shot + “ASKING”; and “TRYAGAIN” replies (seen during slot migration) + are retried after a short back-off. Full-table operations scan + every master and merge the results. + + Only master nodes are used; the module always reads from and + writes to masters to guarantee read-your-writes consistency. + +1.4. Dependencies + +1.4.1. OpenSIPS Modules + + The following modules must be loaded before this module: + * No dependencies on other OpenSIPS modules. + +1.4.2. External Libraries or Applications + + The following libraries or applications must be installed + before running OpenSIPS with this module loaded: + * hiredis - the minimalistic Redis client C library + (development package, typically libhiredis-dev). + +1.5. Exported Parameters + +1.5.1. connect_timeout (integer) + + The timeout, in milliseconds, for establishing a TCP connection + to a Redis node. It also bounds how long a failover blackout + can stall an operation before it is retried or failed. + + Default value is 1000 (1 second). + + Example 1.1. Set connect_timeout parameter +... +modparam("db_redis", "connect_timeout", 500) +... + +1.5.2. query_timeout (integer) + + The send/receive timeout, in milliseconds, applied to commands + issued on an established connection. Keep it small enough that + a stalled node does not block OpenSIPS worker processes for + long, especially when the dialog module is used in realtime + db_mode. + + Default value is 2000 (2 seconds). + + Example 1.2. Set query_timeout parameter +... +modparam("db_redis", "query_timeout", 1000) +... + +1.5.3. scan_count (integer) + + The “COUNT” hint passed to the Redis “SCAN” command when + iterating a whole table (used by full-table loads and by + queries that are not addressed by primary key). Larger values + reduce the number of round-trips at the cost of larger + individual replies. + + Default value is 100. + + Example 1.3. Set scan_count parameter +... +modparam("db_redis", "scan_count", 500) +... + +1.5.4. mode (string) + + Pins the connection mode instead of auto-detecting it. Accepted + values are: + * auto - detect single vs. cluster from “INFO CLUSTER” (the + default). + * single - force single-endpoint mode; startup fails if the + endpoint is a cluster node. + * cluster - force cluster mode; startup fails if the endpoint + has cluster support disabled. + + Pinning the mode turns an accidental misconfiguration (for + example a db_url pointing at the wrong kind of server) into a + clear startup error instead of a silently wrong runtime + behaviour. + + Default value is "auto". + + Example 1.4. Set mode parameter +... +modparam("db_redis", "mode", "cluster") +... + +1.6. Exported Functions + + No function exported to be used from the configuration file. + +1.7. Usage + + The module is used by pointing a consuming module's db_url at a + redis:// URL. The URL has the standard OpenSIPS form + “redis://[:password@]host[:port]/db”. The database path + component is mandatory for the URL parser but is only + meaningful for a standalone server (it selects the logical + database number); it is ignored in cluster mode. A + password-only Redis is authenticated with the “:password@” + form. + + Example 1.5. Persisting the dialog table in a single Redis + server +... +loadmodule "db_redis/db_redis.so" +loadmodule "dialog/dialog.so" + +modparam("dialog", "db_mode", 1) +modparam("dialog", "db_url", "redis://127.0.0.1:6379/0") +... + + Example 1.6. Persisting the dialog table in a Redis Cluster +... +loadmodule "db_redis/db_redis.so" +loadmodule "dialog/dialog.so" + +modparam("dialog", "db_mode", 1) +# a single cluster node is enough as a seed; the rest of the topology +# is discovered automatically +modparam("dialog", "db_url", "redis://:secretpass@10.0.0.20:6379/0") +... + + Before starting OpenSIPS, provision the table schemas into + Redis, for example (add “-c” when talking to a cluster): +redis-cli [-c] < scripts/redis/dialog-create.redis + +1.8. Limitations + + * Queries support equality and the ordering operators (“=”, + “<”, “>”, “<=”, “>=”, “!=”) on any column, combined with + AND or OR, plus a single “ORDER BY” column. Lookups + addressed by the primary key are served directly; every + other query performs a table scan and filters in the + client. There is no “JOIN” support (the OpenSIPS DB API + does not provide one) and no raw-query support. + * Updating the value of the primary-key column of an existing + row is not supported. + * The module is intended for modules that use the structured + DB API with per-row primary-key access, such as dialog, + dispatcher, clusterer and usrloc. Raw SQL queries are not + implemented; a module that relies on them works only if it + also offers a structured-query path (as usrloc does for + contact loading). + * As with the SQL backends, sharing rows between several + OpenSIPS instances through the same Redis store requires + the instances to use a matching configuration (for the + dialog table, an identical hash_size and matching listening + sockets). + +1.9. Installation + + Because it depends on an external library, the db_redis module + is not compiled and installed by default. You can use one of + the next options. + * - edit the "Makefile" and remove "db_redis" from + "excluded_modules" list. Then follow the standard procedure + to install OpenSIPS: "make all; make install". + * - from command line use: 'make all + include_modules="db_redis"; make install + include_modules="db_redis"'. + +Chapter 2. Contributors + +2.1. By Commit Statistics + + Table 2.1. Top contributors by DevScore^(1), authored + commits^(2) and lines added/removed^(3) + Name DevScore Commits Lines ++ Lines -- + 1. Yury Kirsanov 27 2 2802 4 + + (1) DevScore = author_commits + author_lines_added / + (project_lines_added / project_commits) + author_lines_deleted + / (project_lines_deleted / project_commits) + + (2) including any documentation-related commits, excluding + merge commits. Regarding imported patches/code, we do our best + to count the work on behalf of the proper owner, as per the + "fix_authors" and "mod_renames" arrays in + opensips/docs/build-contrib.sh. If you identify any + patches/commits which do not get properly attributed to you, + please submit a pull request which extends "fix_authors" and/or + "mod_renames". + + (3) ignoring whitespace edits, renamed files and auto-generated + files + +2.2. By Commit Activity + + Table 2.2. Most recently active contributors^(1) to this module + Name Commit Activity + 1. Yury Kirsanov Jul 2026 - Jul 2026 + + (1) including any documentation-related commits, excluding + merge commits + +Chapter 3. Documentation + +3.1. Contributors + + Documentation Copyrights: + + Copyright © 2026 www.opensips-solutions.com diff --git a/modules/db_redis/db_redis.c b/modules/db_redis/db_redis.c new file mode 100644 index 00000000000..2d84ab986aa --- /dev/null +++ b/modules/db_redis/db_redis.c @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2026 OpenSIPS Solutions + * + * This file is part of opensips, a free SIP server. + * + * opensips is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version + * + * opensips is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "../../sr_module.h" +#include "../../db/db.h" +#include "db_redis.h" +#include "dbase.h" + +int rdb_connect_timeout = 1000; /* ms */ +int rdb_query_timeout = 2000; /* ms */ +int rdb_scan_count = 100; +int rdb_mode = RDB_MODE_AUTO; + +static char *rdb_mode_str; + +static int redis_mod_init(void); + +static const cmd_export_t cmds[] = { + {"db_bind_api", (cmd_function)db_redis_bind_api, {{0,0,0}},0}, + {0,0,{{0,0,0}},0} +}; + +static const param_export_t params[] = { + {"connect_timeout", INT_PARAM, &rdb_connect_timeout}, + {"query_timeout", INT_PARAM, &rdb_query_timeout}, + {"scan_count", INT_PARAM, &rdb_scan_count}, + {"mode", STR_PARAM, &rdb_mode_str}, + {0, 0, 0} +}; + +struct module_exports exports = { + "db_redis", + MOD_TYPE_SQLDB, /* class of this module */ + MODULE_VERSION, + DEFAULT_DLFLAGS, /* dlopen flags */ + 0, /* load function */ + NULL, /* OpenSIPS module dependencies */ + cmds, + 0, /* exported async functions */ + params, /* module parameters */ + 0, /* exported statistics */ + 0, /* exported MI functions */ + 0, /* exported pseudo-variables */ + 0, /* exported transformations */ + 0, /* extra processes */ + 0, /* module pre-initialization function */ + redis_mod_init, /* module initialization function */ + 0, /* response function */ + 0, /* destroy function */ + 0, /* per-child init function */ + 0 /* reload confirm function */ +}; + +static int redis_mod_init(void) +{ + if (rdb_mode_str) { + if (strcasecmp(rdb_mode_str, "auto") == 0) + rdb_mode = RDB_MODE_AUTO; + else if (strcasecmp(rdb_mode_str, "single") == 0) + rdb_mode = RDB_MODE_SINGLE; + else if (strcasecmp(rdb_mode_str, "cluster") == 0) + rdb_mode = RDB_MODE_CLUSTER; + else { + LM_ERR("invalid mode parameter <%s>, use one of " + "auto|single|cluster\n", rdb_mode_str); + return -1; + } + } + + if (rdb_connect_timeout <= 0 || rdb_query_timeout <= 0) { + LM_ERR("connect_timeout and query_timeout must be positive\n"); + return -1; + } + if (rdb_scan_count <= 0) + rdb_scan_count = 100; + + return 0; +} + +int db_redis_bind_api(const str* mod, db_func_t *dbb) +{ + if (dbb == NULL) + return -1; + + memset(dbb, 0, sizeof(db_func_t)); + + dbb->use_table = db_redis_use_table; + dbb->init = db_redis_init; + dbb->close = db_redis_close; + dbb->query = db_redis_query; + dbb->free_result = db_redis_free_result; + dbb->insert = db_redis_insert; + dbb->delete = db_redis_delete; + dbb->update = db_redis_update; + dbb->replace = db_redis_replace; + dbb->insert_update = db_redis_insert_update; + dbb->last_inserted_id = db_redis_last_inserted_id; + /* no raw_query / fetch_result / async in phase 1 - the core derives + * the exported capabilities from the functions set above */ + + return 0; +} diff --git a/modules/db_redis/db_redis.h b/modules/db_redis/db_redis.h new file mode 100644 index 00000000000..49150427dcb --- /dev/null +++ b/modules/db_redis/db_redis.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2026 OpenSIPS Solutions + * + * This file is part of opensips, a free SIP server. + * + * opensips is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version + * + * opensips is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef DB_REDIS_H +#define DB_REDIS_H + +/* connection mode selection (modparam "mode") */ +#define RDB_MODE_AUTO 0 +#define RDB_MODE_SINGLE 1 +#define RDB_MODE_CLUSTER 2 + +extern int rdb_connect_timeout; /* ms */ +extern int rdb_query_timeout; /* ms */ +extern int rdb_scan_count; /* SCAN COUNT hint */ +extern int rdb_mode; /* RDB_MODE_* */ + +int db_redis_bind_api(const str* mod, db_func_t *dbb); + +#endif /* DB_REDIS_H */ diff --git a/modules/db_redis/dbase.c b/modules/db_redis/dbase.c new file mode 100644 index 00000000000..84173ff1642 --- /dev/null +++ b/modules/db_redis/dbase.c @@ -0,0 +1,1339 @@ +/* + * Copyright (C) 2026 OpenSIPS Solutions + * + * This file is part of opensips, a free SIP server. + * + * opensips is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version + * + * opensips is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include + +#include "../../dprint.h" +#include "../../mem/mem.h" +#include "../../ut.h" +#include "../../db/db.h" +#include "../../db/db_ut.h" +#include "../../db/db_pool.h" +#include "db_redis.h" +#include "redis_con.h" +#include "schema.h" +#include "dbase.h" + +#define RDB_KEY_MAX 320 /*
+ ':' + */ +#define RDB_NUM_MAX 32 /* printed numeric value */ + +enum rdb_opc { + RDB_OP_EQ, RDB_OP_LT, RDB_OP_GT, RDB_OP_LEQ, RDB_OP_GEQ, RDB_OP_NEQ +}; + +struct rdb_filter { + int col; /* schema column index */ + enum rdb_opc op; + const db_val_t *val; +}; + +/* one matched row while collecting; vals[i].s==NULL means SQL NULL, + * non-NULL pointers are individual pkg allocations (NUL-terminated) */ +struct rdb_irow { + struct rdb_irow *next; + str *vals; +}; + + +db_con_t* db_redis_init(const str* _url) +{ + return db_do_init(_url, (void *)db_redis_new_connection); +} + +void db_redis_close(db_con_t* _h) +{ + db_do_close(_h, db_redis_free_connection); +} + +int db_redis_use_table(db_con_t* _h, const str* _t) +{ + return db_use_table(_h, _t); +} + +int db_redis_free_result(db_con_t* _h, db_res_t* _r) +{ + if (!_r) + return -1; + return db_free_result(_r); +} + + +static int rdb_parse_opc(const db_op_t op, enum rdb_opc *out) +{ + if (!op || strcmp(op, OP_EQ) == 0) + *out = RDB_OP_EQ; + else if (strcmp(op, OP_LT) == 0) + *out = RDB_OP_LT; + else if (strcmp(op, OP_GT) == 0) + *out = RDB_OP_GT; + else if (strcmp(op, OP_LEQ) == 0) + *out = RDB_OP_LEQ; + else if (strcmp(op, OP_GEQ) == 0) + *out = RDB_OP_GEQ; + else if (strcmp(op, OP_NEQ) == 0) + *out = RDB_OP_NEQ; + else { + LM_ERR("unsupported operator <%s>\n", op); + return -1; + } + return 0; +} + + +/* render a db value as its storage string; numeric types print into buf */ +static int rdb_val2str(const db_val_t *v, str *out, char *buf) +{ + if (VAL_NULL(v)) { + out->s = NULL; + out->len = 0; + return 0; + } + + out->len = RDB_NUM_MAX; + out->s = buf; + + switch (VAL_TYPE(v)) { + case DB_INT: + if (db_int2str(VAL_INT(v), buf, &out->len) < 0) + return -1; + break; + case DB_BITMAP: + if (db_int2str((int)VAL_BITMAP(v), buf, &out->len) < 0) + return -1; + break; + case DB_BIGINT: + if (db_bigint2str(VAL_BIGINT(v), buf, &out->len) < 0) + return -1; + break; + case DB_DOUBLE: + if (db_double2str(VAL_DOUBLE(v), buf, &out->len) < 0) + return -1; + break; + case DB_DATETIME: + /* stored as a Unix timestamp, so it stays numerically + * comparable and sortable */ + if (db_bigint2str((long long)VAL_TIME(v), buf, &out->len) < 0) + return -1; + break; + case DB_STRING: + out->s = (char *)VAL_STRING(v); + out->len = out->s ? strlen(out->s) : 0; + break; + case DB_STR: + out->s = VAL_STR(v).s; + out->len = VAL_STR(v).len; + break; + case DB_BLOB: + out->s = VAL_BLOB(v).s; + out->len = VAL_BLOB(v).len; + break; + default: + LM_ERR("unsupported value type %d\n", VAL_TYPE(v)); + return -1; + } + return 0; +} + + +static inline int rdb_row_key(const str *table, const str *pkval, + char *buf, str *out) +{ + if (table->len + 1 + pkval->len >= RDB_KEY_MAX) { + LM_ERR("row key too long for table <%.*s>\n", + table->len, table->s); + return -1; + } + memcpy(buf, table->s, table->len); + buf[table->len] = ':'; + memcpy(buf + table->len + 1, pkval->s, pkval->len); + out->s = buf; + out->len = table->len + 1 + pkval->len; + return 0; +} + + +/* look up a field in a HGETALL reply; returns 0 and fills out when + * present, -1 when the field is absent (SQL NULL) */ +static int rdb_reply_field(redisReply *hg, const str *name, str *out) +{ + size_t i; + + for (i = 0; i+1 < hg->elements; i += 2) { + if (hg->element[i]->type != REDIS_REPLY_STRING) + continue; + if ((int)hg->element[i]->len == name->len && + memcmp(hg->element[i]->str, name->s, name->len) == 0) { + if (hg->element[i+1]->type != REDIS_REPLY_STRING) + return -1; + out->s = hg->element[i+1]->str; + out->len = hg->element[i+1]->len; + return 0; + } + } + return -1; +} + + +static inline int rdb_col_is_numeric(const struct rdb_col *col) +{ + return col->type == DB_INT || col->type == DB_BIGINT || + col->type == DB_DOUBLE || col->type == DB_DATETIME || + col->type == DB_BITMAP; +} + + +/* evaluate one filter against a fetched row; SQL comparison semantics: + * NULL compares false to everything, except "= NULL" (IS NULL) and + * "!= NULL" (IS NOT NULL) */ +static int rdb_eval_one(redisReply *hg, const struct rdb_schema *sch, + const struct rdb_filter *f) +{ + const struct rdb_col *col = &sch->cols[f->col]; + str field, want; + char buf[RDB_NUM_MAX]; + int c, have; + + have = (rdb_reply_field(hg, &col->name, &field) == 0); + + if (VAL_NULL(f->val)) { + if (f->op == RDB_OP_EQ) + return !have; + if (f->op == RDB_OP_NEQ) + return have; + return 0; + } + if (!have) + return f->op == RDB_OP_NEQ ? 0 : 0; + + if (rdb_val2str(f->val, &want, buf) < 0) + return 0; + + if (rdb_col_is_numeric(col)) { + if (col->type == DB_DOUBLE) { + double a = strtod(field.s, NULL); + double b = strtod(want.s, NULL); + c = (a < b) ? -1 : (a > b) ? 1 : 0; + } else { + long long a = strtoll(field.s, NULL, 10); + long long b = strtoll(want.s, NULL, 10); + c = (a < b) ? -1 : (a > b) ? 1 : 0; + } + } else { + int min = field.len < want.len ? field.len : want.len; + c = memcmp(field.s, want.s, min); + if (c == 0) + c = (field.len < want.len) ? -1 : + (field.len > want.len) ? 1 : 0; + } + + switch (f->op) { + case RDB_OP_EQ: return c == 0; + case RDB_OP_NEQ: return c != 0; + case RDB_OP_LT: return c < 0; + case RDB_OP_GT: return c > 0; + case RDB_OP_LEQ: return c <= 0; + case RDB_OP_GEQ: return c >= 0; + } + return 0; +} + + +static int rdb_eval(redisReply *hg, const struct rdb_schema *sch, + const struct rdb_filter *flt, int nf, int is_or) +{ + int i, m; + + if (nf == 0) + return 1; + + for (i = 0; i < nf; i++) { + m = rdb_eval_one(hg, sch, &flt[i]); + if (is_or && m) + return 1; + if (!is_or && !m) + return 0; + } + return is_or ? 0 : 1; +} + + +/* build the filter array out of _k/_op/_v; returns 0 or -1 */ +static int rdb_build_filters(const struct rdb_schema *sch, + const db_key_t* _k, const db_op_t* _op, const db_val_t* _v, + int _n, struct rdb_filter *flt) +{ + int i; + + for (i = 0; i < _n; i++) { + flt[i].col = rdb_schema_col(sch, _k[i]); + if (flt[i].col < 0) { + LM_ERR("unknown column <%.*s> in table <%.*s>\n", + _k[i]->len, _k[i]->s, sch->table.len, sch->table.s); + return -1; + } + if (rdb_parse_opc(_op ? _op[i] : NULL, &flt[i].op) < 0) + return -1; + flt[i].val = &_v[i]; + } + return 0; +} + + +/* single "pk = value" fast path detector: returns the filter index of + * the pk-EQ filter usable for direct addressing, or -1 */ +static int rdb_pk_fastpath(const struct rdb_schema *sch, + const struct rdb_filter *flt, int nf, int is_or) +{ + int i; + + if (nf == 0) + return -1; + /* with OR semantics a single condition behaves like AND */ + if (is_or && nf > 1) + return -1; + + for (i = 0; i < nf; i++) + if (flt[i].col == sch->pk && flt[i].op == RDB_OP_EQ && + !VAL_NULL(flt[i].val)) + return i; + return -1; +} + + +/* fetch one row hash by its key; returns reply (may be empty array + * when the row does not exist) or NULL on transport error */ +static redisReply *rdb_fetch_row(struct redis_con *con, const str *rowkey) +{ + const char *argv[2]; + size_t argvlen[2]; + redisReply *reply; + + argv[0] = "HGETALL"; argvlen[0] = 7; + argv[1] = rowkey->s; argvlen[1] = rowkey->len; + + reply = rdb_cmd_key(con, rowkey, 2, argv, argvlen); + if (reply && reply->type == REDIS_REPLY_ERROR) { + LM_ERR("HGETALL <%.*s> failed: %s\n", + rowkey->len, rowkey->s, reply->str); + freeReplyObject(reply); + return NULL; + } + return reply; +} + + +/* ---------------- table scanning ---------------- */ + +typedef int (*rdb_scan_cb)(struct redis_con *con, const str *rowkey, + redisReply *hg, void *arg); + +/* walk all rows of a table across all masters, invoking cb for each + * existing row hash; cb return <0 aborts the scan */ +static int rdb_scan_table(struct redis_con *con, const str *table, + rdb_scan_cb cb, void *arg) +{ + redis_node *node; + redisReply *reply, *hg; + char pattern[RDB_KEY_MAX], cursor[24]; + const char *argv[6]; + size_t argvlen[6], k; + char countbuf[16]; + str rowkey; + int rc; + + if (table->len + 2 >= RDB_KEY_MAX) { + LM_ERR("table name too long\n"); + return -1; + } + memcpy(pattern, table->s, table->len); + pattern[table->len] = ':'; + pattern[table->len+1] = '*'; + + snprintf(countbuf, sizeof countbuf, "%d", rdb_scan_count); + + for (node = con->nodes; node; node = node->next) { + strcpy(cursor, "0"); + do { + argv[0] = "SCAN"; argvlen[0] = 4; + argv[1] = cursor; argvlen[1] = strlen(cursor); + argv[2] = "MATCH"; argvlen[2] = 5; + argv[3] = pattern; argvlen[3] = table->len + 2; + argv[4] = "COUNT"; argvlen[4] = 5; + argv[5] = countbuf; argvlen[5] = strlen(countbuf); + + reply = rdb_cmd_node(con, node, 6, argv, argvlen); + if (!reply || reply->type != REDIS_REPLY_ARRAY || + reply->elements < 2 || + reply->element[0]->type != REDIS_REPLY_STRING || + reply->element[1]->type != REDIS_REPLY_ARRAY) { + LM_ERR("SCAN failed on %s:%u\n", node->host, node->port); + if (reply) + freeReplyObject(reply); + return -1; + } + + strncpy(cursor, reply->element[0]->str, sizeof(cursor)-1); + cursor[sizeof(cursor)-1] = 0; + + for (k = 0; k < reply->element[1]->elements; k++) { + if (reply->element[1]->element[k]->type != + REDIS_REPLY_STRING) + continue; + rowkey.s = reply->element[1]->element[k]->str; + rowkey.len = reply->element[1]->element[k]->len; + + hg = rdb_fetch_row(con, &rowkey); + if (!hg) + continue; + if (hg->type == REDIS_REPLY_ARRAY && hg->elements) { + rc = cb(con, &rowkey, hg, arg); + if (rc < 0) { + freeReplyObject(hg); + freeReplyObject(reply); + return -1; + } + } + freeReplyObject(hg); + } + freeReplyObject(reply); + } while (strcmp(cursor, "0") != 0); + } + + return 0; +} + + +/* ---------------- query ---------------- */ + +struct rdb_query_ctx { + const struct rdb_schema *sch; + const struct rdb_filter *flt; + int nf; + int is_or; + const int *rescols; /* schema col index per result column */ + int nrescols; + struct rdb_irow *head, *tail; + int count; +}; + +static void rdb_free_irows(struct rdb_irow *head, int ncols) +{ + struct rdb_irow *next; + int i; + + while (head) { + next = head->next; + for (i = 0; i < ncols; i++) + if (head->vals[i].s) + pkg_free(head->vals[i].s); + pkg_free(head); + head = next; + } +} + +/* copy the requested columns of a row-hash reply into an irow */ +static struct rdb_irow *rdb_extract_row(redisReply *hg, + const struct rdb_schema *sch, const int *rescols, int nrescols) +{ + struct rdb_irow *row; + str field; + int i; + + row = pkg_malloc(sizeof *row + nrescols * sizeof(str)); + if (!row) { + LM_ERR("no more pkg memory for result row\n"); + return NULL; + } + memset(row, 0, sizeof *row + nrescols * sizeof(str)); + row->vals = (str *)(row + 1); + + for (i = 0; i < nrescols; i++) { + if (rdb_reply_field(hg, &sch->cols[rescols[i]].name, &field) < 0) + continue; /* stays NULL */ + row->vals[i].s = pkg_malloc(field.len + 1); + if (!row->vals[i].s) { + LM_ERR("no more pkg memory for column value\n"); + rdb_free_irows(row, nrescols); + return NULL; + } + memcpy(row->vals[i].s, field.s, field.len); + row->vals[i].s[field.len] = 0; + row->vals[i].len = field.len; + } + return row; +} + +static int rdb_query_scan_cb(struct redis_con *con, const str *rowkey, + redisReply *hg, void *arg) +{ + struct rdb_query_ctx *ctx = (struct rdb_query_ctx *)arg; + struct rdb_irow *row; + + if (!rdb_eval(hg, ctx->sch, ctx->flt, ctx->nf, ctx->is_or)) + return 0; + + row = rdb_extract_row(hg, ctx->sch, ctx->rescols, ctx->nrescols); + if (!row) + return -1; + + if (ctx->tail) + ctx->tail->next = row; + else + ctx->head = row; + ctx->tail = row; + ctx->count++; + return 0; +} + +/* qsort context for ORDER BY (each process is single-threaded) */ +static int rdb_sort_idx; +static int rdb_sort_desc; +static int rdb_sort_numeric; + +static int rdb_sort_cmp(const void *pa, const void *pb) +{ + const struct rdb_irow *a = *(struct rdb_irow * const *)pa; + const struct rdb_irow *b = *(struct rdb_irow * const *)pb; + const str *va = &a->vals[rdb_sort_idx]; + const str *vb = &b->vals[rdb_sort_idx]; + int c, min; + + /* NULLs sort first */ + if (!va->s || !vb->s) + c = (!va->s && !vb->s) ? 0 : (!va->s ? -1 : 1); + else if (rdb_sort_numeric) { + double da = strtod(va->s, NULL), db_ = strtod(vb->s, NULL); + c = (da < db_) ? -1 : (da > db_) ? 1 : 0; + } else { + min = va->len < vb->len ? va->len : vb->len; + c = memcmp(va->s, vb->s, min); + if (c == 0) + c = (va->len < vb->len) ? -1 : (va->len > vb->len) ? 1 : 0; + } + return rdb_sort_desc ? -c : c; +} + +/* fill one db_res value from its stored string representation */ +static int rdb_fill_val(db_val_t *val, db_type_t type, str *stored) +{ + VAL_TYPE(val) = type; + VAL_FREE(val) = 0; + + if (!stored->s) { + VAL_NULL(val) = 1; + return 0; + } + VAL_NULL(val) = 0; + + switch (type) { + case DB_INT: + if (db_str2int(stored->s, &VAL_INT(val)) < 0) + goto badval; + break; + case DB_BIGINT: + if (db_str2bigint(stored->s, &VAL_BIGINT(val)) < 0) + goto badval; + break; + case DB_DOUBLE: + if (db_str2double(stored->s, &VAL_DOUBLE(val)) < 0) + goto badval; + break; + case DB_DATETIME: { + /* stored as a Unix timestamp (see rdb_val2str) */ + long long ts; + if (db_str2bigint(stored->s, &ts) < 0) + goto badval; + VAL_TIME(val) = (time_t)ts; + break; + } + case DB_STR: + VAL_STR(val).s = stored->s; + VAL_STR(val).len = stored->len; + VAL_FREE(val) = 1; + stored->s = NULL; /* ownership moved to the result */ + break; + case DB_BLOB: + VAL_BLOB(val).s = stored->s; + VAL_BLOB(val).len = stored->len; + VAL_FREE(val) = 1; + stored->s = NULL; + break; + default: + LM_ERR("unsupported column type %d\n", type); + VAL_NULL(val) = 1; + return -1; + } + return 0; + +badval: + LM_ERR("failed to parse stored value <%.*s> as type %d\n", + stored->len, stored->s, type); + VAL_NULL(val) = 1; + return -1; +} + + +int db_redis_query(const db_con_t* _h, const db_key_t* _k, const db_op_t* _op, + const db_val_t* _v, const db_key_t* _c, const int _n, const int _nc, + const db_key_t _o, db_res_t** _r) +{ + struct redis_con *con; + struct rdb_schema *sch; + struct rdb_filter *flt = NULL; + struct rdb_query_ctx ctx; + struct rdb_irow **sorted = NULL, *irow; + db_res_t *res = NULL; + redisReply *hg; + int *rescols = NULL; + int i, r, fp, is_or, order_idx; + char numbuf[RDB_NUM_MAX], keybuf[RDB_KEY_MAX]; + str pkval, rowkey, ocol; + char *p; + + if (!_h || !CON_TABLE(_h) || !_r) { + LM_ERR("invalid query parameters\n"); + return -1; + } + *_r = NULL; + memset(&ctx, 0, sizeof ctx); + + con = CON_REDIS(_h); + is_or = (_h->flags & CON_OR_OPERATOR) ? 1 : 0; + CON_OR_RESET(_h); + + rdb_maybe_refresh(con); + + sch = rdb_get_schema(con, CON_TABLE(_h)); + if (!sch) + return -1; + + /* the result column set */ + if (_c && _nc > 0) { + rescols = pkg_malloc(_nc * sizeof(int)); + if (!rescols) + goto oom; + for (i = 0; i < _nc; i++) { + rescols[i] = rdb_schema_col(sch, _c[i]); + if (rescols[i] < 0) { + LM_ERR("unknown column <%.*s> in table <%.*s>\n", + _c[i]->len, _c[i]->s, + sch->table.len, sch->table.s); + goto error; + } + } + ctx.nrescols = _nc; + } else { + rescols = pkg_malloc(sch->nr_cols * sizeof(int)); + if (!rescols) + goto oom; + for (i = 0; i < sch->nr_cols; i++) + rescols[i] = i; + ctx.nrescols = sch->nr_cols; + } + + if (_n > 0) { + flt = pkg_malloc(_n * sizeof *flt); + if (!flt) + goto oom; + if (rdb_build_filters(sch, _k, _op, _v, _n, flt) < 0) + goto error; + } + + ctx.sch = sch; + ctx.flt = flt; + ctx.nf = _n; + ctx.is_or = is_or; + ctx.rescols = rescols; + + fp = flt ? rdb_pk_fastpath(sch, flt, _n, is_or) : -1; + if (fp >= 0) { + /* direct row addressing by primary key */ + if (rdb_val2str(flt[fp].val, &pkval, numbuf) < 0 || + rdb_row_key(CON_TABLE(_h), &pkval, keybuf, &rowkey) < 0) + goto error; + + hg = rdb_fetch_row(con, &rowkey); + if (!hg) + goto error; + if (hg->type == REDIS_REPLY_ARRAY && hg->elements) { + if (rdb_query_scan_cb(con, &rowkey, hg, &ctx) < 0) { + freeReplyObject(hg); + goto error; + } + } + freeReplyObject(hg); + } else { + if (rdb_scan_table(con, CON_TABLE(_h), + rdb_query_scan_cb, &ctx) < 0) + goto error; + } + + /* ORDER BY: single column, optional ASC/DESC */ + if (_o && _o->s && ctx.count > 1) { + ocol.s = _o->s; + p = memchr(_o->s, ' ', _o->len); + ocol.len = p ? (int)(p - _o->s) : _o->len; + + order_idx = -1; + for (i = 0; i < ctx.nrescols; i++) + if (sch->cols[rescols[i]].name.len == ocol.len && + memcmp(sch->cols[rescols[i]].name.s, ocol.s, ocol.len) == 0) { + order_idx = i; + break; + } + if (order_idx < 0) { + LM_WARN("order-by column <%.*s> not in the result set, " + "ignoring ordering\n", ocol.len, ocol.s); + } else { + rdb_sort_idx = order_idx; + rdb_sort_numeric = + rdb_col_is_numeric(&sch->cols[rescols[order_idx]]); + rdb_sort_desc = 0; + if (p) { + while (p < _o->s + _o->len && *p == ' ') p++; + if (_o->s + _o->len - p >= 4 && + strncasecmp(p, "DESC", 4) == 0) + rdb_sort_desc = 1; + } + + sorted = pkg_malloc(ctx.count * sizeof *sorted); + if (!sorted) + goto oom; + for (irow = ctx.head, i = 0; irow; irow = irow->next) + sorted[i++] = irow; + qsort(sorted, ctx.count, sizeof *sorted, rdb_sort_cmp); + /* relink in sorted order */ + for (i = 0; i < ctx.count-1; i++) + sorted[i]->next = sorted[i+1]; + sorted[ctx.count-1]->next = NULL; + ctx.head = sorted[0]; + pkg_free(sorted); + sorted = NULL; + } + } + + /* build the db_res_t */ + res = db_new_result(); + if (!res) + goto oom; + RES_COL_N(res) = ctx.nrescols; + if (db_allocate_columns(res, ctx.nrescols) < 0) + goto oom; + for (i = 0; i < ctx.nrescols; i++) { + RES_NAMES(res)[i] = &sch->cols[rescols[i]].name; + RES_TYPES(res)[i] = sch->cols[rescols[i]].type; + } + + if (ctx.count > 0) { + if (db_allocate_rows(res, ctx.count) < 0) + goto oom; + for (irow = ctx.head, r = 0; irow; irow = irow->next, r++) { + ROW_N(&RES_ROWS(res)[r]) = ctx.nrescols; + for (i = 0; i < ctx.nrescols; i++) + rdb_fill_val(&ROW_VALUES(&RES_ROWS(res)[r])[i], + sch->cols[rescols[i]].type, &irow->vals[i]); + } + } + RES_ROW_N(res) = ctx.count; + RES_NUM_ROWS(res) = ctx.count; + RES_LAST_ROW(res) = ctx.count; + + rdb_free_irows(ctx.head, ctx.nrescols); + if (flt) + pkg_free(flt); + pkg_free(rescols); + + *_r = res; + return 0; + +oom: + LM_ERR("no more pkg memory while running query\n"); +error: + if (res) + db_free_result(res); + if (sorted) + pkg_free(sorted); + rdb_free_irows(ctx.head, ctx.nrescols); + if (flt) + pkg_free(flt); + if (rescols) + pkg_free(rescols); + return -1; +} + + +/* ---------------- write operations ---------------- */ + +/* fetch the next auto-increment value for a table */ +static int rdb_next_id(struct redis_con *con, const str *table, + char *numbuf, str *out) +{ + char keybuf[RDB_KEY_MAX]; + str key; + const char *argv[2]; + size_t argvlen[2]; + redisReply *reply; + + if (table->len + 4 >= RDB_KEY_MAX) + return -1; + memcpy(keybuf, "seq:", 4); + memcpy(keybuf + 4, table->s, table->len); + key.s = keybuf; + key.len = 4 + table->len; + + argv[0] = "INCR"; argvlen[0] = 4; + argv[1] = key.s; argvlen[1] = key.len; + + reply = rdb_cmd_key(con, &key, 2, argv, argvlen); + if (!reply || reply->type != REDIS_REPLY_INTEGER) { + LM_ERR("INCR on <%.*s> failed\n", key.len, key.s); + if (reply) + freeReplyObject(reply); + return -1; + } + con->last_insert_id = reply->integer; + out->len = RDB_NUM_MAX; + out->s = numbuf; + if (db_bigint2str(reply->integer, numbuf, &out->len) < 0) { + freeReplyObject(reply); + return -1; + } + freeReplyObject(reply); + return 0; +} + + +#define RDB_STORE_INSERT 0 /* fail when the row already exists */ +#define RDB_STORE_REPLACE 1 /* delete any existing row first */ +#define RDB_STORE_MERGE 2 /* merge fields into any existing row */ + +static int rdb_store_row(const db_con_t* _h, const db_key_t* _k, + const db_val_t* _v, const int _n, int store_mode) +{ + struct redis_con *con; + struct rdb_schema *sch; + redisReply *reply; + const char **argv = NULL; + size_t *argvlen = NULL; + char (*numbufs)[RDB_NUM_MAX] = NULL; + char keybuf[RDB_KEY_MAX], pknum[RDB_NUM_MAX]; + str pkval = STR_NULL, rowkey, v; + int i, c, argc, pk_arg = -1, rc = -1; + + if (!_h || !CON_TABLE(_h) || !_k || !_v || _n <= 0) { + LM_ERR("invalid insert parameters\n"); + return -1; + } + + con = CON_REDIS(_h); + rdb_maybe_refresh(con); + + sch = rdb_get_schema(con, CON_TABLE(_h)); + if (!sch) + return -1; + + /* locate the primary key among the given columns */ + for (i = 0; i < _n; i++) { + c = rdb_schema_col(sch, _k[i]); + if (c < 0) { + LM_ERR("unknown column <%.*s> in table <%.*s>\n", + _k[i]->len, _k[i]->s, sch->table.len, sch->table.s); + return -1; + } + if (c == sch->pk && !VAL_NULL(&_v[i])) + pk_arg = i; + } + + if (pk_arg >= 0) { + if (rdb_val2str(&_v[pk_arg], &pkval, pknum) < 0) + return -1; + } else { + if (!sch->cols[sch->pk].is_auto) { + LM_ERR("no value for primary key <%.*s> of table <%.*s>\n", + sch->cols[sch->pk].name.len, sch->cols[sch->pk].name.s, + sch->table.len, sch->table.s); + return -1; + } + if (rdb_next_id(con, CON_TABLE(_h), pknum, &pkval) < 0) + return -1; + } + + if (rdb_row_key(CON_TABLE(_h), &pkval, keybuf, &rowkey) < 0) + return -1; + + argv = pkg_malloc((4 + 2*_n) * sizeof *argv); + argvlen = pkg_malloc((4 + 2*_n) * sizeof *argvlen); + numbufs = pkg_malloc(_n * sizeof *numbufs); + if (!argv || !argvlen || !numbufs) { + LM_ERR("no more pkg memory for insert\n"); + goto out; + } + + if (store_mode == RDB_STORE_REPLACE) { + argv[0] = "DEL"; argvlen[0] = 3; + argv[1] = rowkey.s; argvlen[1] = rowkey.len; + reply = rdb_cmd_key(con, &rowkey, 2, argv, argvlen); + if (!reply) { + LM_ERR("failed to clear row for replace\n"); + goto out; + } + freeReplyObject(reply); + } + + if (store_mode != RDB_STORE_MERGE) { + /* reserve the row: HSETNX doubles as + * both the uniqueness check and the guaranteed first field */ + argv[0] = "HSETNX"; argvlen[0] = 6; + argv[1] = rowkey.s; argvlen[1] = rowkey.len; + argv[2] = sch->cols[sch->pk].name.s; + argvlen[2] = sch->cols[sch->pk].name.len; + argv[3] = pkval.s; argvlen[3] = pkval.len; + + reply = rdb_cmd_key(con, &rowkey, 4, argv, argvlen); + if (!reply || reply->type != REDIS_REPLY_INTEGER) { + LM_ERR("row reservation failed for <%.*s>\n", + rowkey.len, rowkey.s); + if (reply) + freeReplyObject(reply); + goto out; + } + if (reply->integer == 0) { + LM_ERR("duplicate primary key <%.*s> in table <%.*s>\n", + pkval.len, pkval.s, sch->table.len, sch->table.s); + freeReplyObject(reply); + goto out; + } + freeReplyObject(reply); + } + + /* store the remaining non-NULL fields */ + argc = 2; + argv[0] = "HSET"; argvlen[0] = 4; + argv[1] = rowkey.s; argvlen[1] = rowkey.len; + + if (store_mode == RDB_STORE_MERGE) { + /* the pk field is written here instead of via HSETNX */ + argv[argc] = sch->cols[sch->pk].name.s; + argvlen[argc] = sch->cols[sch->pk].name.len; + argc++; + argv[argc] = pkval.s; + argvlen[argc] = pkval.len; + argc++; + } + + for (i = 0; i < _n; i++) { + if (i == pk_arg || VAL_NULL(&_v[i])) + continue; + if (rdb_val2str(&_v[i], &v, numbufs[i]) < 0) + goto out; + argv[argc] = _k[i]->s; + argvlen[argc] = _k[i]->len; + argc++; + argv[argc] = v.s ? v.s : ""; + argvlen[argc] = v.len; + argc++; + } + + if (argc > 2) { + reply = rdb_cmd_key(con, &rowkey, argc, argv, argvlen); + if (!reply || reply->type == REDIS_REPLY_ERROR) { + LM_ERR("failed to store row <%.*s> (%s)\n", + rowkey.len, rowkey.s, reply ? reply->str : "io error"); + if (reply) + freeReplyObject(reply); + goto out; + } + freeReplyObject(reply); + } + + rc = 0; +out: + if (argv) pkg_free(argv); + if (argvlen) pkg_free(argvlen); + if (numbufs) pkg_free(numbufs); + return rc; +} + + +int db_redis_insert(const db_con_t* _h, const db_key_t* _k, + const db_val_t* _v, const int _n) +{ + return rdb_store_row(_h, _k, _v, _n, RDB_STORE_INSERT); +} + +int db_redis_replace(const db_con_t* _h, const db_key_t* _k, + const db_val_t* _v, const int _n) +{ + return rdb_store_row(_h, _k, _v, _n, RDB_STORE_REPLACE); +} + +int db_redis_insert_update(const db_con_t* _h, const db_key_t* _k, + const db_val_t* _v, const int _n) +{ + return rdb_store_row(_h, _k, _v, _n, RDB_STORE_MERGE); +} + +int db_redis_last_inserted_id(const db_con_t* _h) +{ + if (!_h) + return -1; + return (int)CON_REDIS(_h)->last_insert_id; +} + + +/* ---------------- delete ---------------- */ + +static int rdb_del_key(struct redis_con *con, const str *rowkey) +{ + const char *argv[2]; + size_t argvlen[2]; + redisReply *reply; + + argv[0] = "DEL"; argvlen[0] = 3; + argv[1] = rowkey->s; argvlen[1] = rowkey->len; + + reply = rdb_cmd_key(con, rowkey, 2, argv, argvlen); + if (!reply || reply->type == REDIS_REPLY_ERROR) { + LM_ERR("DEL <%.*s> failed\n", rowkey->len, rowkey->s); + if (reply) + freeReplyObject(reply); + return -1; + } + freeReplyObject(reply); + return 0; +} + +struct rdb_del_ctx { + const struct rdb_schema *sch; + const struct rdb_filter *flt; + int nf; + int is_or; + int failed; +}; + +static int rdb_delete_scan_cb(struct redis_con *con, const str *rowkey, + redisReply *hg, void *arg) +{ + struct rdb_del_ctx *ctx = (struct rdb_del_ctx *)arg; + + if (!rdb_eval(hg, ctx->sch, ctx->flt, ctx->nf, ctx->is_or)) + return 0; + if (rdb_del_key(con, rowkey) < 0) + ctx->failed = 1; + return 0; +} + +int db_redis_delete(const db_con_t* _h, const db_key_t* _k, + const db_op_t* _o, const db_val_t* _v, const int _n) +{ + struct redis_con *con; + struct rdb_schema *sch; + struct rdb_filter *flt = NULL; + struct rdb_del_ctx ctx; + char numbuf[RDB_NUM_MAX], keybuf[RDB_KEY_MAX]; + str pkval, rowkey; + int i, is_or, all_pk_eq, rc = -1; + + if (!_h || !CON_TABLE(_h)) { + LM_ERR("invalid delete parameters\n"); + return -1; + } + + con = CON_REDIS(_h); + is_or = (_h->flags & CON_OR_OPERATOR) ? 1 : 0; + CON_OR_RESET(_h); + + rdb_maybe_refresh(con); + + sch = rdb_get_schema(con, CON_TABLE(_h)); + if (!sch) + return -1; + + if (_n > 0) { + flt = pkg_malloc(_n * sizeof *flt); + if (!flt) { + LM_ERR("no more pkg memory for delete\n"); + return -1; + } + if (rdb_build_filters(sch, _k, _o, _v, _n, flt) < 0) + goto out; + + /* pk-only deletes go straight to the row keys: a single + * "pk=X", or an OR-list of "pk=X" conditions (bulk delete) */ + all_pk_eq = 1; + for (i = 0; i < _n; i++) + if (flt[i].col != sch->pk || flt[i].op != RDB_OP_EQ || + VAL_NULL(flt[i].val)) { + all_pk_eq = 0; + break; + } + + if (all_pk_eq && (_n == 1 || is_or)) { + for (i = 0; i < _n; i++) { + if (rdb_val2str(flt[i].val, &pkval, numbuf) < 0 || + rdb_row_key(CON_TABLE(_h), &pkval, keybuf, + &rowkey) < 0) + goto out; + if (rdb_del_key(con, &rowkey) < 0) + goto out; + } + rc = 0; + goto out; + } + } + + ctx.sch = sch; + ctx.flt = flt; + ctx.nf = _n; + ctx.is_or = is_or; + ctx.failed = 0; + + if (rdb_scan_table(con, CON_TABLE(_h), rdb_delete_scan_cb, &ctx) < 0) + goto out; + + rc = ctx.failed ? -1 : 0; +out: + if (flt) + pkg_free(flt); + return rc; +} + + +/* ---------------- update ---------------- */ + +struct rdb_upd_ctx { + const struct rdb_schema *sch; + const struct rdb_filter *flt; + int nf; + int is_or; + /* collected row keys (own pkg copies) */ + str *keys; + int count; + int alloc; +}; + +static int rdb_update_scan_cb(struct redis_con *con, const str *rowkey, + redisReply *hg, void *arg) +{ + struct rdb_upd_ctx *ctx = (struct rdb_upd_ctx *)arg; + str *newkeys; + + if (!rdb_eval(hg, ctx->sch, ctx->flt, ctx->nf, ctx->is_or)) + return 0; + + if (ctx->count == ctx->alloc) { + ctx->alloc = ctx->alloc ? 2*ctx->alloc : 16; + newkeys = pkg_realloc(ctx->keys, ctx->alloc * sizeof(str)); + if (!newkeys) { + LM_ERR("no more pkg memory for update targets\n"); + return -1; + } + ctx->keys = newkeys; + } + ctx->keys[ctx->count].s = pkg_malloc(rowkey->len); + if (!ctx->keys[ctx->count].s) { + LM_ERR("no more pkg memory for update target key\n"); + return -1; + } + memcpy(ctx->keys[ctx->count].s, rowkey->s, rowkey->len); + ctx->keys[ctx->count].len = rowkey->len; + ctx->count++; + return 0; +} + +/* apply the SET clause to one row: HSET the non-NULL values, + * HDEL the NULL ones */ +static int rdb_apply_update(struct redis_con *con, const str *rowkey, + const db_key_t* _uk, const db_val_t* _uv, int _un, + char (*numbufs)[RDB_NUM_MAX]) +{ + const char *argv[2 + 2*_un]; + size_t argvlen[2 + 2*_un]; + redisReply *reply; + str v; + int i, argc; + + /* HSET phase */ + argc = 2; + argv[0] = "HSET"; argvlen[0] = 4; + argv[1] = rowkey->s; argvlen[1] = rowkey->len; + for (i = 0; i < _un; i++) { + if (VAL_NULL(&_uv[i])) + continue; + if (rdb_val2str(&_uv[i], &v, numbufs[i]) < 0) + return -1; + argv[argc] = _uk[i]->s; + argvlen[argc] = _uk[i]->len; + argc++; + argv[argc] = v.s ? v.s : ""; + argvlen[argc] = v.len; + argc++; + } + if (argc > 2) { + reply = rdb_cmd_key(con, rowkey, argc, argv, argvlen); + if (!reply || reply->type == REDIS_REPLY_ERROR) { + LM_ERR("failed to update row <%.*s> (%s)\n", rowkey->len, + rowkey->s, reply ? reply->str : "io error"); + if (reply) + freeReplyObject(reply); + return -1; + } + freeReplyObject(reply); + } + + /* HDEL phase for NULL assignments */ + argc = 2; + argv[0] = "HDEL"; argvlen[0] = 4; + argv[1] = rowkey->s; argvlen[1] = rowkey->len; + for (i = 0; i < _un; i++) { + if (!VAL_NULL(&_uv[i])) + continue; + argv[argc] = _uk[i]->s; + argvlen[argc] = _uk[i]->len; + argc++; + } + if (argc > 2) { + reply = rdb_cmd_key(con, rowkey, argc, argv, argvlen); + if (!reply || reply->type == REDIS_REPLY_ERROR) { + LM_ERR("failed to clear columns of row <%.*s>\n", + rowkey->len, rowkey->s); + if (reply) + freeReplyObject(reply); + return -1; + } + freeReplyObject(reply); + } + + return 0; +} + +int db_redis_update(const db_con_t* _h, const db_key_t* _k, + const db_op_t* _o, const db_val_t* _v, const db_key_t* _uk, + const db_val_t* _uv, const int _n, const int _un) +{ + struct redis_con *con; + struct rdb_schema *sch; + struct rdb_filter *flt = NULL; + struct rdb_upd_ctx ctx; + redisReply *hg; + char (*numbufs)[RDB_NUM_MAX] = NULL; + char numbuf[RDB_NUM_MAX], keybuf[RDB_KEY_MAX]; + str pkval, rowkey; + int i, c, fp, is_or, rc = -1; + + if (!_h || !CON_TABLE(_h) || !_uk || !_uv || _un <= 0) { + LM_ERR("invalid update parameters\n"); + return -1; + } + + con = CON_REDIS(_h); + is_or = (_h->flags & CON_OR_OPERATOR) ? 1 : 0; + CON_OR_RESET(_h); + + rdb_maybe_refresh(con); + + sch = rdb_get_schema(con, CON_TABLE(_h)); + if (!sch) + return -1; + + /* validate the SET columns; updating the pk would require a key + * rename and is not supported */ + for (i = 0; i < _un; i++) { + c = rdb_schema_col(sch, _uk[i]); + if (c < 0) { + LM_ERR("unknown column <%.*s> in table <%.*s>\n", + _uk[i]->len, _uk[i]->s, sch->table.len, sch->table.s); + return -1; + } + if (c == sch->pk) { + LM_ERR("updating the primary key column <%.*s> is " + "not supported\n", _uk[i]->len, _uk[i]->s); + return -1; + } + } + + memset(&ctx, 0, sizeof ctx); + ctx.sch = sch; + ctx.is_or = is_or; + + if (_n > 0) { + flt = pkg_malloc(_n * sizeof *flt); + if (!flt) { + LM_ERR("no more pkg memory for update\n"); + return -1; + } + if (rdb_build_filters(sch, _k, _o, _v, _n, flt) < 0) + goto out; + ctx.flt = flt; + ctx.nf = _n; + } + + numbufs = pkg_malloc(_un * sizeof *numbufs); + if (!numbufs) { + LM_ERR("no more pkg memory for update values\n"); + goto out; + } + + fp = flt ? rdb_pk_fastpath(sch, flt, _n, is_or) : -1; + if (fp >= 0) { + if (rdb_val2str(flt[fp].val, &pkval, numbuf) < 0 || + rdb_row_key(CON_TABLE(_h), &pkval, keybuf, &rowkey) < 0) + goto out; + + hg = rdb_fetch_row(con, &rowkey); + if (!hg) + goto out; + if (hg->type == REDIS_REPLY_ARRAY && hg->elements && + rdb_eval(hg, sch, flt, _n, is_or)) { + freeReplyObject(hg); + if (rdb_apply_update(con, &rowkey, _uk, _uv, _un, + numbufs) < 0) + goto out; + } else { + /* no matching row - not an error, zero rows updated */ + freeReplyObject(hg); + } + rc = 0; + goto out; + } + + if (rdb_scan_table(con, CON_TABLE(_h), rdb_update_scan_cb, &ctx) < 0) + goto out; + + rc = 0; + for (i = 0; i < ctx.count; i++) + if (rdb_apply_update(con, &ctx.keys[i], _uk, _uv, _un, + numbufs) < 0) + rc = -1; + +out: + for (i = 0; i < ctx.count; i++) + if (ctx.keys[i].s) + pkg_free(ctx.keys[i].s); + if (ctx.keys) + pkg_free(ctx.keys); + if (numbufs) + pkg_free(numbufs); + if (flt) + pkg_free(flt); + return rc; +} diff --git a/modules/db_redis/dbase.h b/modules/db_redis/dbase.h new file mode 100644 index 00000000000..d680619e4d2 --- /dev/null +++ b/modules/db_redis/dbase.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2026 OpenSIPS Solutions + * + * This file is part of opensips, a free SIP server. + * + * opensips is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version + * + * opensips is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef DB_REDIS_DBASE_H +#define DB_REDIS_DBASE_H + +#include "../../db/db_con.h" +#include "../../db/db_res.h" +#include "../../db/db_key.h" +#include "../../db/db_op.h" +#include "../../db/db_val.h" + +db_con_t* db_redis_init(const str* _url); +void db_redis_close(db_con_t* _h); +int db_redis_use_table(db_con_t* _h, const str* _t); +int db_redis_free_result(db_con_t* _h, db_res_t* _r); + +int db_redis_query(const db_con_t* _h, const db_key_t* _k, const db_op_t* _op, + const db_val_t* _v, const db_key_t* _c, const int _n, const int _nc, + const db_key_t _o, db_res_t** _r); + +int db_redis_insert(const db_con_t* _h, const db_key_t* _k, + const db_val_t* _v, const int _n); + +int db_redis_delete(const db_con_t* _h, const db_key_t* _k, + const db_op_t* _o, const db_val_t* _v, const int _n); + +int db_redis_update(const db_con_t* _h, const db_key_t* _k, + const db_op_t* _o, const db_val_t* _v, const db_key_t* _uk, + const db_val_t* _uv, const int _n, const int _un); + +int db_redis_replace(const db_con_t* _h, const db_key_t* _k, + const db_val_t* _v, const int _n); + +int db_redis_insert_update(const db_con_t* _h, const db_key_t* _k, + const db_val_t* _v, const int _n); + +int db_redis_last_inserted_id(const db_con_t* _h); + +#endif /* DB_REDIS_DBASE_H */ diff --git a/modules/db_redis/doc/contributors.xml b/modules/db_redis/doc/contributors.xml new file mode 100644 index 00000000000..f4e23055b44 --- /dev/null +++ b/modules/db_redis/doc/contributors.xml @@ -0,0 +1,79 @@ + + + &contributors; + +
+ By Commit Statistics + +
Top contributors by DevScore<superscript>(1)</superscript>, authored commits<superscript>(2)</superscript> and lines added/removed<superscript>(3)</superscript> + + + + + Name + DevScore + Commits + Lines ++ + Lines -- + + + + + 1. + Yury Kirsanov + 27 + 2 + 2802 + 4 + + + +
+ + + (1) DevScore = author_commits + author_lines_added / (project_lines_added / project_commits) + author_lines_deleted / (project_lines_deleted / project_commits) + + + (2) including any documentation-related commits, excluding merge commits. Regarding imported patches/code, we do our best to count the work on behalf of the proper owner, as per the "fix_authors" and "mod_renames" arrays in opensips/docs/build-contrib.sh. If you identify any patches/commits which do not get properly attributed to you, please submit a pull request which extends "fix_authors" and/or "mod_renames". + + + (3) ignoring whitespace edits, renamed files and auto-generated files + + + +
+ By Commit Activity + + Most recently active contributors<superscript>(1)</superscript> to this module + + + + + Name + Commit Activity + + + + + 1. + Yury Kirsanov + Jul 2026 - Jul 2026 + + + +
+ + + (1) including any documentation-related commits, excluding merge commits + +
+ + + + Documentation +
+ Contributors + +
+ +
diff --git a/modules/db_redis/doc/db_redis.xml b/modules/db_redis/doc/db_redis.xml new file mode 100644 index 00000000000..495263444a8 --- /dev/null +++ b/modules/db_redis/doc/db_redis.xml @@ -0,0 +1,30 @@ + + + + + + + +%docentities; + +]> + + + + db_redis Module + &osipsname; + + + + &admin; + &faq; + &contrib; + + &docCopyrights; + ©right; 2026 &osipssol; + + diff --git a/modules/db_redis/doc/db_redis_admin.xml b/modules/db_redis/doc/db_redis_admin.xml new file mode 100644 index 00000000000..4c700cda614 --- /dev/null +++ b/modules/db_redis/doc/db_redis_admin.xml @@ -0,0 +1,393 @@ + + + + + &adminguide; + +
+ Overview + + This module provides a &osips; DB API backend that stores data in + a Redis server or a Redis + Cluster. It implements the structured database interface used by + modules such as dialog, so that their tables + can be persisted in Redis instead of a traditional SQL database. + + + Unlike the cachedb_redis module, which exposes + Redis through the key-value NoSQL interface, db_redis + presents Redis as a relational-style backend behind the standard + db_url mechanism. A module is switched to Redis + simply by pointing its db_url at a + redis:// URL; no code change is required in the + consuming module. + + + The connection type is auto-detected at startup: the module works + either against a single Redis endpoint (a standalone server, or the + current master reached through an external load balancer/Sentinel + setup) or against a fully-enabled Redis Cluster, where it discovers + the whole topology and keeps a connection open to every master + shard. + +
+ +
+ Data Model + + Redis is schemaless, so the module needs the table layout to be + declared out of band. Both the schema definitions and the data are + stored as Redis hashes: + + + + + Schema - the hash + schema:<table> describes one table. It holds + a __cols field with the ordered, space-separated + list of column names, a __pk field naming the + primary-key column, and one field per column giving its type as + type[,null][,auto]. The supported types are + int, bigint, + double, string, + blob and datetime. The + null attribute marks a nullable column and + auto marks an auto-increment primary key. + + + + + Rows - each row is the hash + <table>:<primary key value>, with one + field per non-NULL column. A NULL column is represented by the + absence of the field. Values of blob columns + are stored verbatim (binary-safe), with no encoding overhead. + + + + + Sequences - auto-increment primary keys are + generated with INCR on the counter + seq:<table>. + + + + + Table version - the standard &osips; + version table is provisioned like any other table, + with one row per served table, for example + HSET version:dialog table_name dialog table_version 12. + + + + + The key prefixes schema: and seq: are + reserved by the module and must not be used as table names. Ready-made + provisioning files are shipped under the scripts/redis/ + directory; see that directory's README for details. A schema is + loaded from Redis on first access to a table and cached for the + lifetime of the connection. + +
+ +
+ Deployment Modes + + At connection time the module issues an INFO CLUSTER + command and inspects the cluster_enabled field to + decide how to operate. The detected behaviour can be pinned with the + parameter. + +
+ Single endpoint + + Used for a standalone Redis server, or when a Redis Sentinel + deployment is fronted by an external load balancer that always + routes to the current master. The module keeps a single + connection and transparently reconnects on I/O errors or when it + receives a -READONLY reply (which indicates it is + talking to a demoted master after a failover). Sentinel discovery + and master election are expected to be handled externally. + +
+
+ Redis Cluster + + When the endpoint is a cluster node, the module fetches the full + topology (via CLUSTER SHARDS, falling back to + CLUSTER SLOTS on older servers) starting from the + single seed host given in db_url. It then opens + and keeps warm a connection to every master shard, so that a key + routed to any shard can be served immediately over an already + established connection rather than paying a new TCP handshake on + each redirect. + + + Each key is routed to its shard by computing the CRC16 hash slot + (honouring {hash tag} braces). MOVED + redirects are served from the warm connection pool and trigger a + deferred topology refresh; ASK redirects are + followed with a one-shot ASKING; and + TRYAGAIN replies (seen during slot migration) are + retried after a short back-off. Full-table operations scan every + master and merge the results. + + + Only master nodes are used; the module always reads from and + writes to masters to guarantee read-your-writes consistency. + +
+
+ +
+ Dependencies +
+ &osips; Modules + + The following modules must be loaded before this module: + + + + No dependencies on other &osips; modules. + + + + +
+
+ External Libraries or Applications + + The following libraries or applications must be installed before running + &osips; with this module loaded: + + + + hiredis - the minimalistic Redis client + C library (development package, typically + libhiredis-dev). + + + + +
+
+ +
+ Exported Parameters + +
+ <varname>connect_timeout</varname> (integer) + + The timeout, in milliseconds, for establishing a TCP connection to a + Redis node. It also bounds how long a failover blackout can stall an + operation before it is retried or failed. + + + Default value is 1000 (1 second). + + + Set <varname>connect_timeout</varname> parameter + +... +modparam("db_redis", "connect_timeout", 500) +... + + +
+ +
+ <varname>query_timeout</varname> (integer) + + The send/receive timeout, in milliseconds, applied to commands issued + on an established connection. Keep it small enough that a stalled node + does not block &osips; worker processes for long, especially when the + dialog module is used in realtime db_mode. + + + Default value is 2000 (2 seconds). + + + Set <varname>query_timeout</varname> parameter + +... +modparam("db_redis", "query_timeout", 1000) +... + + +
+ +
+ <varname>scan_count</varname> (integer) + + The COUNT hint passed to the Redis SCAN + command when iterating a whole table (used by full-table loads and by + queries that are not addressed by primary key). Larger values reduce + the number of round-trips at the cost of larger individual replies. + + + Default value is 100. + + + Set <varname>scan_count</varname> parameter + +... +modparam("db_redis", "scan_count", 500) +... + + +
+ +
+ <varname>mode</varname> (string) + + Pins the connection mode instead of auto-detecting it. Accepted + values are: + + + + auto - detect single vs. cluster from + INFO CLUSTER (the default). + + + single - force single-endpoint mode; + startup fails if the endpoint is a cluster node. + + + cluster - force cluster mode; startup + fails if the endpoint has cluster support disabled. + + + + Pinning the mode turns an accidental misconfiguration (for example a + db_url pointing at the wrong kind of server) into a + clear startup error instead of a silently wrong runtime behaviour. + + + Default value is "auto". + + + Set <varname>mode</varname> parameter + +... +modparam("db_redis", "mode", "cluster") +... + + +
+ +
+ +
+ Exported Functions + + No function exported to be used from the configuration file. + +
+ +
+ Usage + + The module is used by pointing a consuming module's + db_url at a redis:// URL. The + URL has the standard &osips; form + redis://[:password@]host[:port]/db. The database path + component is mandatory for the URL parser but is only meaningful for a + standalone server (it selects the logical database number); it is + ignored in cluster mode. A password-only Redis is authenticated with + the :password@ form. + + + Persisting the dialog table in a single Redis server + +... +loadmodule "db_redis/db_redis.so" +loadmodule "dialog/dialog.so" + +modparam("dialog", "db_mode", 1) +modparam("dialog", "db_url", "redis://127.0.0.1:6379/0") +... + + + + Persisting the dialog table in a Redis Cluster + +... +loadmodule "db_redis/db_redis.so" +loadmodule "dialog/dialog.so" + +modparam("dialog", "db_mode", 1) +# a single cluster node is enough as a seed; the rest of the topology +# is discovered automatically +modparam("dialog", "db_url", "redis://:secretpass@10.0.0.20:6379/0") +... + + + + Before starting &osips;, provision the table schemas into Redis, for + example (add -c when talking to a cluster): + + +redis-cli [-c] < scripts/redis/dialog-create.redis + +
+ +
+ Limitations + + + Queries support equality and the ordering operators + (=, <, >, + <=, >=, !=) on + any column, combined with AND or OR, plus a single + ORDER BY column. Lookups addressed by the primary + key are served directly; every other query performs a table scan + and filters in the client. There is no JOIN support + (the &osips; DB API does not provide one) and no raw-query + support. + + + Updating the value of the primary-key column of an existing row + is not supported. + + + The module is intended for modules that use the structured DB API + with per-row primary-key access, such as + dialog, dispatcher, + clusterer and usrloc. + Raw SQL queries are not implemented; a module that relies on them + works only if it also offers a structured-query path (as + usrloc does for contact loading). + + + As with the SQL backends, sharing rows between several &osips; + instances through the same Redis store requires the instances to + use a matching configuration (for the dialog table, an identical + hash_size and matching listening sockets). + + +
+ +
+ Installation + + Because it depends on an external library, the db_redis module is not + compiled and installed by default. You can use one of the next + options. + + + + + - edit the "Makefile" and remove "db_redis" from + "excluded_modules" list. Then follow the standard procedure to + install &osips;: "make all; make install". + + + + + - from command line use: 'make all include_modules="db_redis"; + make install include_modules="db_redis"'. + + + +
+
diff --git a/modules/db_redis/redis_con.c b/modules/db_redis/redis_con.c new file mode 100644 index 00000000000..dc0beba525b --- /dev/null +++ b/modules/db_redis/redis_con.c @@ -0,0 +1,826 @@ +/* + * Copyright (C) 2026 OpenSIPS Solutions + * + * This file is part of opensips, a free SIP server. + * + * opensips is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version + * + * opensips is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include + +#include "../../dprint.h" +#include "../../mem/mem.h" +#include "../../ut.h" +#include "../../db/db.h" +#include "db_redis.h" +#include "redis_con.h" +#include "schema.h" + +#define RDB_MAX_REDIRECTS 3 + +/* CRC16 (CCITT) as used by Redis Cluster for key hash slots, + * reference implementation from the Redis Cluster specification */ +static const uint16_t crc16tab[256] = { + 0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7, + 0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef, + 0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6, + 0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de, + 0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485, + 0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d, + 0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4, + 0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc, + 0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823, + 0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b, + 0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12, + 0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a, + 0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41, + 0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49, + 0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70, + 0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78, + 0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f, + 0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067, + 0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e, + 0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256, + 0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d, + 0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405, + 0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c, + 0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634, + 0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab, + 0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3, + 0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a, + 0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92, + 0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9, + 0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1, + 0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8, + 0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0 +}; + +static uint16_t rdb_crc16(const char *buf, int len) +{ + int i; + uint16_t crc = 0; + + for (i = 0; i < len; i++) + crc = (crc << 8) ^ crc16tab[((crc >> 8) ^ *buf++) & 0x00FF]; + return crc; +} + +/* hash slot of a key, honoring {hash tags} as per the cluster spec */ +unsigned int rdb_key_slot(const char *key, int len) +{ + int s, e; + + for (s = 0; s < len; s++) + if (key[s] == '{') + break; + + if (s < len) { + for (e = s+1; e < len; e++) + if (key[e] == '}') + break; + if (e < len && e != s+1) + return rdb_crc16(key+s+1, e-s-1) % RDB_NR_SLOTS; + } + + return rdb_crc16(key, len) % RDB_NR_SLOTS; +} + + +static redisContext *rdb_connect_ctx(const char *host, unsigned short port) +{ + struct timeval tv; + redisContext *ctx; + + tv.tv_sec = rdb_connect_timeout / 1000; + tv.tv_usec = (rdb_connect_timeout % 1000) * 1000; + + ctx = redisConnectWithTimeout(host, port, tv); + if (!ctx) { + LM_ERR("failed to allocate redis context for %s:%u\n", host, port); + return NULL; + } + if (ctx->err) { + LM_ERR("failed to connect to redis %s:%u (%s)\n", + host, port, ctx->errstr); + redisFree(ctx); + return NULL; + } + + tv.tv_sec = rdb_query_timeout / 1000; + tv.tv_usec = (rdb_query_timeout % 1000) * 1000; + redisSetTimeout(ctx, tv); + + return ctx; +} + + +/* authenticate + select database on a fresh context, using the db URL id */ +static int rdb_setup_ctx(struct redis_con *con, redisContext *ctx) +{ + redisReply *reply; + int dbno; + str s; + + if (con->id->password) { + if (con->id->username && con->id->username[0]) + reply = redisCommand(ctx, "AUTH %s %s", + con->id->username, con->id->password); + else + reply = redisCommand(ctx, "AUTH %s", con->id->password); + if (!reply || reply->type == REDIS_REPLY_ERROR) { + LM_ERR("redis AUTH failed (%s)\n", + reply ? reply->str : ctx->errstr); + if (reply) freeReplyObject(reply); + return -1; + } + freeReplyObject(reply); + } + + /* database number only makes sense outside cluster mode */ + if (con->mode != RDB_MODE_CLUSTER && + con->id->database && con->id->database[0]) { + s.s = con->id->database; + s.len = strlen(s.s); + if (str2sint(&s, &dbno) == 0 && dbno > 0) { + reply = redisCommand(ctx, "SELECT %d", dbno); + if (!reply || reply->type == REDIS_REPLY_ERROR) { + LM_ERR("redis SELECT %d failed (%s)\n", dbno, + reply ? reply->str : ctx->errstr); + if (reply) freeReplyObject(reply); + return -1; + } + freeReplyObject(reply); + } + } + + return 0; +} + + +static redis_node *rdb_find_node(struct redis_con *con, + const char *host, unsigned short port) +{ + redis_node *n; + + for (n = con->nodes; n; n = n->next) + if (n->port == port && strcmp(n->host, host) == 0) + return n; + return NULL; +} + + +/* add a node to the pool and (eagerly) connect it */ +static redis_node *rdb_add_node(struct redis_con *con, + const char *host, unsigned short port) +{ + redis_node *n; + + n = pkg_malloc(sizeof *n); + if (!n) { + LM_ERR("no more pkg memory for redis node\n"); + return NULL; + } + memset(n, 0, sizeof *n); + strncpy(n->host, host, RDB_HOST_MAX-1); + n->port = port; + + n->ctx = rdb_connect_ctx(n->host, n->port); + if (n->ctx && rdb_setup_ctx(con, n->ctx) < 0) { + redisFree(n->ctx); + n->ctx = NULL; + } + if (!n->ctx) + LM_WARN("redis node %s:%u added disconnected, " + "will retry on first use\n", n->host, n->port); + + n->next = con->nodes; + con->nodes = n; + return n; +} + + +static void rdb_disconnect_node(redis_node *n) +{ + if (n->ctx) { + redisFree(n->ctx); + n->ctx = NULL; + } +} + + +/* (re)establish the TCP connection of a node */ +static int rdb_reconnect_node(struct redis_con *con, redis_node *n) +{ + rdb_disconnect_node(n); + n->ctx = rdb_connect_ctx(n->host, n->port); + if (!n->ctx) + return -1; + if (rdb_setup_ctx(con, n->ctx) < 0) { + rdb_disconnect_node(n); + return -1; + } + return 0; +} + + +/* assign [start,end] slot range to a node */ +static void rdb_map_slots(struct redis_con *con, redis_node *n, + int start, int end) +{ + int i; + + if (start < 0 || end >= RDB_NR_SLOTS || start > end) + return; + for (i = start; i <= end; i++) + con->slot_map[i] = n; +} + + +/* parse one "host port" pair out of a CLUSTER SHARDS node map */ +static int rdb_shards_node_endpoint(redisReply *node_map, + char *host, size_t host_len, unsigned short *port, int *is_master, + int *is_online) +{ + size_t i; + redisReply *k, *v; + + host[0] = 0; + *port = 0; + *is_master = 0; + *is_online = 1; + + if (node_map->type != REDIS_REPLY_ARRAY && + node_map->type != REDIS_REPLY_MAP) + return -1; + + for (i = 0; i+1 < node_map->elements; i += 2) { + k = node_map->element[i]; + v = node_map->element[i+1]; + if (k->type != REDIS_REPLY_STRING) + continue; + if (strcmp(k->str, "ip") == 0 || strcmp(k->str, "endpoint") == 0) { + if (v->type == REDIS_REPLY_STRING && v->str[0] && !host[0]) { + strncpy(host, v->str, host_len-1); + host[host_len-1] = 0; + } + } else if (strcmp(k->str, "port") == 0) { + if (v->type == REDIS_REPLY_INTEGER) + *port = (unsigned short)v->integer; + } else if (strcmp(k->str, "role") == 0) { + if (v->type == REDIS_REPLY_STRING && + strcmp(v->str, "master") == 0) + *is_master = 1; + } else if (strcmp(k->str, "health") == 0) { + if (v->type == REDIS_REPLY_STRING && + strcmp(v->str, "online") != 0) + *is_online = 0; + } + } + + return (host[0] && *port) ? 0 : -1; +} + + +/* build topology from a CLUSTER SHARDS reply (Redis 7.0+) */ +static int rdb_parse_shards(struct redis_con *con, redisReply *reply) +{ + size_t i, j, r; + redisReply *shard, *k, *v, *slots = NULL, *rnodes = NULL; + redis_node *n; + char host[RDB_HOST_MAX]; + unsigned short port; + int is_master, is_online, mapped = 0; + + if (reply->type != REDIS_REPLY_ARRAY || reply->elements == 0) + return -1; + + for (i = 0; i < reply->elements; i++) { + shard = reply->element[i]; + if (shard->type != REDIS_REPLY_ARRAY && + shard->type != REDIS_REPLY_MAP) + continue; + + slots = NULL; + rnodes = NULL; + for (j = 0; j+1 < shard->elements; j += 2) { + k = shard->element[j]; + v = shard->element[j+1]; + if (k->type != REDIS_REPLY_STRING) + continue; + if (strcmp(k->str, "slots") == 0) + slots = v; + else if (strcmp(k->str, "nodes") == 0) + rnodes = v; + } + if (!slots || !rnodes || rnodes->type != REDIS_REPLY_ARRAY) + continue; + + /* find the online master of this shard */ + n = NULL; + for (r = 0; r < rnodes->elements; r++) { + if (rdb_shards_node_endpoint(rnodes->element[r], host, + sizeof host, &port, &is_master, &is_online) < 0) + continue; + if (!is_master || !is_online) + continue; + n = rdb_find_node(con, host, port); + if (!n) + n = rdb_add_node(con, host, port); + break; + } + if (!n) + continue; + + /* slots is a flat array of start,end pairs */ + if (slots->type == REDIS_REPLY_ARRAY) { + for (r = 0; r+1 < slots->elements; r += 2) { + if (slots->element[r]->type != REDIS_REPLY_INTEGER || + slots->element[r+1]->type != REDIS_REPLY_INTEGER) + continue; + rdb_map_slots(con, n, + (int)slots->element[r]->integer, + (int)slots->element[r+1]->integer); + mapped = 1; + } + } + } + + return mapped ? 0 : -1; +} + + +/* build topology from a CLUSTER SLOTS reply (Redis 3.0+) */ +static int rdb_parse_slots(struct redis_con *con, redisReply *reply) +{ + size_t i; + redisReply *range, *master; + redis_node *n; + int start, end, mapped = 0; + + if (reply->type != REDIS_REPLY_ARRAY || reply->elements == 0) + return -1; + + for (i = 0; i < reply->elements; i++) { + range = reply->element[i]; + if (range->type != REDIS_REPLY_ARRAY || range->elements < 3) + continue; + if (range->element[0]->type != REDIS_REPLY_INTEGER || + range->element[1]->type != REDIS_REPLY_INTEGER) + continue; + start = (int)range->element[0]->integer; + end = (int)range->element[1]->integer; + + /* element[2] is the master: [host, port, id...] */ + master = range->element[2]; + if (master->type != REDIS_REPLY_ARRAY || master->elements < 2 || + master->element[0]->type != REDIS_REPLY_STRING || + master->element[1]->type != REDIS_REPLY_INTEGER) + continue; + + n = rdb_find_node(con, master->element[0]->str, + (unsigned short)master->element[1]->integer); + if (!n) + n = rdb_add_node(con, master->element[0]->str, + (unsigned short)master->element[1]->integer); + if (!n) + continue; + + rdb_map_slots(con, n, start, end); + mapped = 1; + } + + return mapped ? 0 : -1; +} + + +/* fetch and apply the full cluster topology using any usable node; + * newly discovered masters are eagerly connected */ +static int rdb_load_topology(struct redis_con *con, redisContext *seed_ctx) +{ + redisReply *reply; + int rc = -1; + + /* CLUSTER SHARDS first (7.0+), CLUSTER SLOTS as fallback */ + reply = redisCommand(seed_ctx, "CLUSTER SHARDS"); + if (reply && reply->type == REDIS_REPLY_ARRAY && reply->elements) { + rc = rdb_parse_shards(con, reply); + freeReplyObject(reply); + if (rc == 0) { + LM_DBG("cluster topology loaded via CLUSTER SHARDS\n"); + return 0; + } + } else if (reply) { + freeReplyObject(reply); + } + + reply = redisCommand(seed_ctx, "CLUSTER SLOTS"); + if (reply && reply->type == REDIS_REPLY_ARRAY && reply->elements) { + rc = rdb_parse_slots(con, reply); + freeReplyObject(reply); + if (rc == 0) { + LM_DBG("cluster topology loaded via CLUSTER SLOTS\n"); + return 0; + } + return -1; + } + if (reply) + freeReplyObject(reply); + + return -1; +} + + +/* refresh the slot map from any connected master; prune masters that + * no longer own slots */ +static int rdb_refresh_topology(struct redis_con *con) +{ + redis_node *n, **prev; + int i, rc = -1; + + memset(con->slot_map, 0, sizeof con->slot_map); + + for (n = con->nodes; n; n = n->next) { + if (!n->ctx && rdb_reconnect_node(con, n) < 0) + continue; + rc = rdb_load_topology(con, n->ctx); + if (rc == 0) + break; + } + if (rc < 0) { + LM_ERR("failed to refresh cluster topology from any known node\n"); + return -1; + } + + /* drop nodes that own no slots anymore (demoted/removed) */ + prev = &con->nodes; + while ((n = *prev) != NULL) { + for (i = 0; i < RDB_NR_SLOTS; i++) + if (con->slot_map[i] == n) + break; + if (i == RDB_NR_SLOTS) { + LM_INFO("dropping redis node %s:%u (owns no slots)\n", + n->host, n->port); + *prev = n->next; + rdb_disconnect_node(n); + pkg_free(n); + } else { + prev = &n->next; + } + } + + con->need_refresh = 0; + return 0; +} + + +void rdb_maybe_refresh(struct redis_con *con) +{ + if (con->mode == RDB_MODE_CLUSTER && con->need_refresh) + rdb_refresh_topology(con); +} + + +/* log the discovered master pool and how many slots each owns; also + * reports whether the warm TCP connection to each master is up */ +static void rdb_log_pool(struct redis_con *con) +{ + redis_node *n; + int i, slots, total_masters = 0, up = 0; + + for (n = con->nodes; n; n = n->next) { + slots = 0; + for (i = 0; i < RDB_NR_SLOTS; i++) + if (con->slot_map[i] == n) + slots++; + total_masters++; + if (n->ctx) + up++; + LM_DBG("redis cluster master %s:%u - %d slots, connection %s\n", + n->host, n->port, slots, n->ctx ? "up" : "down"); + } + LM_INFO("redis cluster pool ready: %d masters discovered, " + "%d warm connections\n", total_masters, up); +} + + +/* determine whether the endpoint is a cluster; the "cluster_enabled" + * field lives in the "Cluster" section of the general INFO command + * (it is not part of CLUSTER INFO) and is reliably reported as + * cluster_enabled:0/1 by both standalone and cluster instances; on any + * error assume non-cluster */ +static int rdb_probe_cluster(redisContext *ctx) +{ + redisReply *reply; + int enabled = 0; + + reply = redisCommand(ctx, "INFO CLUSTER"); + if (!reply) + return 0; + if (reply->type == REDIS_REPLY_STRING || reply->type == REDIS_REPLY_STATUS) { + if (reply->str && strstr(reply->str, "cluster_enabled:1")) + enabled = 1; + } + freeReplyObject(reply); + return enabled; +} + + +struct redis_con* db_redis_new_connection(const struct db_id* id) +{ + struct redis_con *con; + redisContext *seed; + unsigned short port; + int is_cluster; + + if (!id || !id->host) { + LM_ERR("invalid db URL for redis connection\n"); + return NULL; + } + + con = pkg_malloc(sizeof *con); + if (!con) { + LM_ERR("no more pkg memory for redis connection\n"); + return NULL; + } + memset(con, 0, sizeof *con); + con->id = (struct db_id *)id; + con->ref = 1; + con->mode = RDB_MODE_SINGLE; /* until probed otherwise */ + + port = id->port ? id->port : 6379; + + seed = rdb_connect_ctx(id->host, port); + if (!seed) + goto error; + if (rdb_setup_ctx(con, seed) < 0) { + redisFree(seed); + goto error; + } + + is_cluster = rdb_probe_cluster(seed); + + if (rdb_mode == RDB_MODE_SINGLE && is_cluster) { + LM_ERR("mode pinned to single but %s:%u is a redis cluster node\n", + id->host, port); + redisFree(seed); + goto error; + } + if (rdb_mode == RDB_MODE_CLUSTER && !is_cluster) { + LM_ERR("mode pinned to cluster but %s:%u has cluster " + "support disabled\n", id->host, port); + redisFree(seed); + goto error; + } + + if (is_cluster) { + con->mode = RDB_MODE_CLUSTER; + /* discover all masters and connect them eagerly, so MOVED + * redirects can be served over already-open connections */ + if (rdb_load_topology(con, seed) < 0) { + LM_ERR("failed to load cluster topology from %s:%u\n", + id->host, port); + redisFree(seed); + goto error; + } + /* the seed served its purpose; the pool holds the masters + * (the seed itself is in the pool if it is a master) */ + redisFree(seed); + LM_INFO("connected to redis cluster via seed %s:%u\n", + id->host, port); + rdb_log_pool(con); + } else { + con->mode = RDB_MODE_SINGLE; + /* the seed becomes the single pooled node */ + if (!rdb_add_node(con, id->host, port)) { + redisFree(seed); + goto error; + } + /* rdb_add_node opened its own ctx; keep that one, drop seed */ + redisFree(seed); + if (!con->nodes->ctx) { + LM_ERR("failed to connect single redis node %s:%u\n", + id->host, port); + goto error; + } + LM_INFO("connected to redis server %s:%u\n", id->host, port); + } + + return con; + +error: + db_redis_free_connection((struct pool_con *)con); + return NULL; +} + + +void db_redis_free_connection(struct pool_con* pcon) +{ + struct redis_con *con = (struct redis_con *)pcon; + redis_node *n, *next; + + if (!con) + return; + + for (n = con->nodes; n; n = next) { + next = n->next; + rdb_disconnect_node(n); + pkg_free(n); + } + rdb_free_schemas(con); + pkg_free(con); +} + + +/* pick the node a key routes to */ +static redis_node *rdb_route(struct redis_con *con, const str *key) +{ + redis_node *n; + + if (con->mode != RDB_MODE_CLUSTER) + return con->nodes; + + n = con->slot_map[rdb_key_slot(key->s, key->len)]; + if (!n) { + /* unknown slot owner - any node will answer or redirect us */ + con->need_refresh = 1; + n = con->nodes; + } + return n; +} + + +/* parse "MOVED :" / "ASK :" */ +static int rdb_parse_redirect(const char *err, int *slot, + char *host, size_t host_len, unsigned short *port) +{ + const char *p, *colon; + size_t hlen; + + p = strchr(err, ' '); + if (!p) + return -1; + *slot = atoi(p+1); + p = strchr(p+1, ' '); + if (!p) + return -1; + p++; + colon = strrchr(p, ':'); + if (!colon || colon == p) + return -1; + hlen = colon - p; + if (hlen >= host_len) + return -1; + memcpy(host, p, hlen); + host[hlen] = 0; + *port = (unsigned short)atoi(colon+1); + return (*port != 0) ? 0 : -1; +} + + +redisReply *rdb_cmd_node(struct redis_con *con, redis_node *node, + int argc, const char **argv, const size_t *argvlen) +{ + redisReply *reply; + int retried = 0; + +again: + if (!node->ctx && rdb_reconnect_node(con, node) < 0) + return NULL; + + reply = redisCommandArgv(node->ctx, argc, argv, argvlen); + if (!reply) { + /* I/O error - reconnect once (covers LB-reaped idle + * connections and failed-over endpoints) */ + LM_DBG("redis I/O error on %s:%u (%s), reconnecting\n", + node->host, node->port, + node->ctx ? node->ctx->errstr : "no ctx"); + rdb_disconnect_node(node); + if (!retried) { + retried = 1; + goto again; + } + LM_ERR("redis command failed on %s:%u after reconnect\n", + node->host, node->port); + return NULL; + } + + /* -READONLY: we are talking to a demoted master (e.g. the LB has + * not yet cut over) - reconnect through the same endpoint once */ + if (reply->type == REDIS_REPLY_ERROR && reply->str && + strncmp(reply->str, "READONLY", 8) == 0 && !retried) { + LM_INFO("redis node %s:%u went read-only, reconnecting\n", + node->host, node->port); + freeReplyObject(reply); + rdb_disconnect_node(node); + if (con->mode == RDB_MODE_CLUSTER) + con->need_refresh = 1; + retried = 1; + goto again; + } + + return reply; +} + + +redisReply *rdb_cmd_key(struct redis_con *con, const str *key, + int argc, const char **argv, const size_t *argvlen) +{ + redisReply *reply; + redis_node *node, *target; + char host[RDB_HOST_MAX]; + unsigned short port; + int slot, redirects; + const char *asking = "ASKING"; + size_t asking_len = 6; + redisReply *ask_reply; + + node = rdb_route(con, key); + if (!node) { + LM_ERR("no redis node available\n"); + return NULL; + } + + for (redirects = 0; redirects <= RDB_MAX_REDIRECTS; redirects++) { + + reply = rdb_cmd_node(con, node, argc, argv, argvlen); + if (!reply) + return NULL; + + if (reply->type != REDIS_REPLY_ERROR || !reply->str) + return reply; + + if (strncmp(reply->str, "MOVED ", 6) == 0 && + con->mode == RDB_MODE_CLUSTER) { + if (rdb_parse_redirect(reply->str, &slot, host, + sizeof host, &port) < 0) { + LM_ERR("unparsable MOVED reply <%s>\n", reply->str); + return reply; + } + freeReplyObject(reply); + + /* serve the redirect from the warm pool when possible */ + target = rdb_find_node(con, host, port); + if (!target) + target = rdb_add_node(con, host, port); + if (!target) + return NULL; + if (slot >= 0 && slot < RDB_NR_SLOTS) + con->slot_map[slot] = target; + /* the map is stale beyond this slot - refresh before + * the next operation, not in the hot path */ + con->need_refresh = 1; + node = target; + continue; + } + + if (strncmp(reply->str, "ASK ", 4) == 0 && + con->mode == RDB_MODE_CLUSTER) { + if (rdb_parse_redirect(reply->str, &slot, host, + sizeof host, &port) < 0) { + LM_ERR("unparsable ASK reply <%s>\n", reply->str); + return reply; + } + freeReplyObject(reply); + + /* one-shot redirect: ASKING + command, no map change */ + target = rdb_find_node(con, host, port); + if (!target) + target = rdb_add_node(con, host, port); + if (!target) + return NULL; + ask_reply = rdb_cmd_node(con, target, 1, &asking, &asking_len); + if (ask_reply) + freeReplyObject(ask_reply); + node = target; + continue; + } + + if (strncmp(reply->str, "TRYAGAIN", 8) == 0 && + con->mode == RDB_MODE_CLUSTER) { + /* slot migration in progress; brief backoff and retry */ + freeReplyObject(reply); + usleep(20000); + continue; + } + + /* genuine command error - hand it to the caller */ + return reply; + } + + LM_ERR("redis redirect limit exceeded for key <%.*s>\n", + key->len, key->s); + return NULL; +} diff --git a/modules/db_redis/redis_con.h b/modules/db_redis/redis_con.h new file mode 100644 index 00000000000..4fbec487a98 --- /dev/null +++ b/modules/db_redis/redis_con.h @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2026 OpenSIPS Solutions + * + * This file is part of opensips, a free SIP server. + * + * opensips is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version + * + * opensips is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef REDIS_CON_H +#define REDIS_CON_H + +#include + +/* RESP3 map replies only exist in hiredis >= 1.0; with the default RESP2 + * protocol these arrive as flat arrays, so on older hiredis the constant + * just needs to exist for the (never-matching) comparisons */ +#ifndef REDIS_REPLY_MAP +#define REDIS_REPLY_MAP 9 +#endif + +#include "../../db/db_pool.h" +#include "../../db/db_id.h" +#include "../../str.h" + +#define RDB_NR_SLOTS 16384 + +#define RDB_HOST_MAX 128 + +/* one TCP connection to one Redis server (a cluster master, or the + * single endpoint in non-cluster mode) */ +typedef struct redis_node { + char host[RDB_HOST_MAX]; + unsigned short port; + redisContext *ctx; /* NULL when disconnected */ + struct redis_node *next; +} redis_node; + +struct rdb_schema; + +struct redis_con { + struct db_id* id; /**< Connection identifier */ + unsigned int ref; /**< Reference count */ + struct pool_con *async_pool; /**< Subpool of identical database handles */ + int no_transfers; /**< Number of async queries to this backend */ + struct db_transfer *transfers; /**< Array of ongoing async operations */ + struct pool_con *next; /**< Next element in the pool (different id) */ + + int mode; /* RDB_MODE_SINGLE or RDB_MODE_CLUSTER */ + redis_node *nodes; /* master pool (single mode: one node) */ + redis_node *slot_map[RDB_NR_SLOTS]; /* cluster: slot owner (NULL=unknown) */ + int need_refresh; /* topology refresh requested (cluster) */ + + struct rdb_schema *schemas; /* cached table schemas */ + long long last_insert_id; /* per-connection last auto-generated id */ +}; + +#define CON_REDIS(db_con) ((struct redis_con *)((db_con)->tail)) + +struct redis_con* db_redis_new_connection(const struct db_id* id); +void db_redis_free_connection(struct pool_con* con); + +/* run a command routed by 'key' (row key), redirect- and reconnect-aware; + * returns a reply that the caller must freeReplyObject(), or NULL */ +redisReply *rdb_cmd_key(struct redis_con *con, const str *key, + int argc, const char **argv, const size_t *argvlen); + +/* run a command on an explicit node, with one reconnect attempt */ +redisReply *rdb_cmd_node(struct redis_con *con, redis_node *node, + int argc, const char **argv, const size_t *argvlen); + +/* attempt a topology refresh if one was requested (cluster mode) */ +void rdb_maybe_refresh(struct redis_con *con); + +unsigned int rdb_key_slot(const char *key, int len); + +#endif /* REDIS_CON_H */ diff --git a/modules/db_redis/schema.c b/modules/db_redis/schema.c new file mode 100644 index 00000000000..424b749ec26 --- /dev/null +++ b/modules/db_redis/schema.c @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2026 OpenSIPS Solutions + * + * This file is part of opensips, a free SIP server. + * + * opensips is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version + * + * opensips is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include + +#include "../../dprint.h" +#include "../../mem/mem.h" +#include "../../ut.h" +#include "schema.h" + +#define SCHEMA_KEY_PREFIX "schema:" +#define SCHEMA_KEY_PREFIX_LEN (sizeof(SCHEMA_KEY_PREFIX)-1) + +static int rdb_parse_type(const char *s, size_t len, struct rdb_col *col) +{ + const char *p, *end = s + len; + + col->nullable = 0; + col->is_auto = 0; + + p = memchr(s, ',', len); + if (!p) + p = end; + + if (p-s == 3 && strncmp(s, "int", 3) == 0) + col->type = DB_INT; + else if (p-s == 6 && strncmp(s, "bigint", 6) == 0) + col->type = DB_BIGINT; + else if (p-s == 6 && strncmp(s, "double", 6) == 0) + col->type = DB_DOUBLE; + else if (p-s == 6 && strncmp(s, "string", 6) == 0) + col->type = DB_STR; + else if (p-s == 4 && strncmp(s, "blob", 4) == 0) + col->type = DB_BLOB; + else if (p-s == 8 && strncmp(s, "datetime", 8) == 0) + col->type = DB_DATETIME; + else + return -1; + + while (p < end) { + p++; /* skip ',' */ + if (end-p >= 4 && strncmp(p, "null", 4) == 0) + col->nullable = 1; + else if (end-p >= 4 && strncmp(p, "auto", 4) == 0) + col->is_auto = 1; + p = memchr(p, ',', end-p); + if (!p) + break; + } + + return 0; +} + + +static struct rdb_schema *rdb_load_schema(struct redis_con *con, + const str *table) +{ + struct rdb_schema *sch = NULL; + redisReply *reply = NULL, *k, *v; + char keybuf[SCHEMA_KEY_PREFIX_LEN + 64]; + str key; + const char *argv[2]; + size_t argvlen[2]; + str cols_spec = STR_NULL, pk_name = STR_NULL; + str name; + char *p, *end; + size_t i; + int c; + + if (table->len > 64) { + LM_ERR("table name too long <%.*s>\n", table->len, table->s); + return NULL; + } + + memcpy(keybuf, SCHEMA_KEY_PREFIX, SCHEMA_KEY_PREFIX_LEN); + memcpy(keybuf + SCHEMA_KEY_PREFIX_LEN, table->s, table->len); + key.s = keybuf; + key.len = SCHEMA_KEY_PREFIX_LEN + table->len; + + argv[0] = "HGETALL"; argvlen[0] = 7; + argv[1] = key.s; argvlen[1] = key.len; + + reply = rdb_cmd_key(con, &key, 2, argv, argvlen); + if (!reply) { + LM_ERR("failed to fetch schema for table <%.*s>\n", + table->len, table->s); + return NULL; + } + if (reply->type != REDIS_REPLY_ARRAY || reply->elements == 0) { + LM_ERR("no schema provisioned for table <%.*s> " + "(missing hash <%.*s>)\n", + table->len, table->s, key.len, key.s); + goto error; + } + + /* first pass: locate __cols and __pk */ + for (i = 0; i+1 < reply->elements; i += 2) { + k = reply->element[i]; + v = reply->element[i+1]; + if (k->type != REDIS_REPLY_STRING || v->type != REDIS_REPLY_STRING) + continue; + if (k->len == 6 && strncmp(k->str, "__cols", 6) == 0) { + cols_spec.s = v->str; + cols_spec.len = v->len; + } else if (k->len == 4 && strncmp(k->str, "__pk", 4) == 0) { + pk_name.s = v->str; + pk_name.len = v->len; + } + } + if (!cols_spec.s || !pk_name.s) { + LM_ERR("schema of table <%.*s> lacks __cols or __pk\n", + table->len, table->s); + goto error; + } + + sch = pkg_malloc(sizeof *sch + table->len); + if (!sch) + goto oom; + memset(sch, 0, sizeof *sch); + sch->table.s = (char *)(sch + 1); + sch->table.len = table->len; + memcpy(sch->table.s, table->s, table->len); + sch->pk = -1; + + /* count columns in __cols */ + p = cols_spec.s; + end = cols_spec.s + cols_spec.len; + while (p < end) { + while (p < end && *p == ' ') p++; + if (p == end) break; + sch->nr_cols++; + while (p < end && *p != ' ') p++; + } + if (sch->nr_cols == 0) { + LM_ERR("empty __cols in schema of table <%.*s>\n", + table->len, table->s); + goto error; + } + + sch->cols = pkg_malloc(sch->nr_cols * sizeof *sch->cols); + if (!sch->cols) + goto oom; + memset(sch->cols, 0, sch->nr_cols * sizeof *sch->cols); + + /* second pass: fill the ordered columns, resolving each type */ + p = cols_spec.s; + c = 0; + while (p < end && c < sch->nr_cols) { + while (p < end && *p == ' ') p++; + if (p == end) break; + name.s = p; + while (p < end && *p != ' ') p++; + name.len = p - name.s; + + sch->cols[c].name.s = pkg_malloc(name.len + 1); + if (!sch->cols[c].name.s) + goto oom; + memcpy(sch->cols[c].name.s, name.s, name.len); + sch->cols[c].name.s[name.len] = 0; + sch->cols[c].name.len = name.len; + + /* find the matching type field */ + sch->cols[c].type = DB_INT; + for (i = 0; i+1 < reply->elements; i += 2) { + k = reply->element[i]; + v = reply->element[i+1]; + if (k->type != REDIS_REPLY_STRING || + v->type != REDIS_REPLY_STRING) + continue; + if ((int)k->len == name.len && + memcmp(k->str, name.s, name.len) == 0) { + if (rdb_parse_type(v->str, v->len, &sch->cols[c]) < 0) { + LM_ERR("bad type <%.*s> for column <%.*s> in " + "table <%.*s>\n", (int)v->len, v->str, + name.len, name.s, table->len, table->s); + goto error; + } + break; + } + } + if (i+1 >= reply->elements) { + LM_ERR("column <%.*s> in __cols has no type field in " + "schema of table <%.*s>\n", + name.len, name.s, table->len, table->s); + goto error; + } + + if (name.len == pk_name.len && + memcmp(name.s, pk_name.s, name.len) == 0) + sch->pk = c; + c++; + } + + if (sch->pk < 0) { + LM_ERR("__pk column <%.*s> is not part of __cols in " + "table <%.*s>\n", pk_name.len, pk_name.s, + table->len, table->s); + goto error; + } + + freeReplyObject(reply); + + sch->next = con->schemas; + con->schemas = sch; + + LM_DBG("loaded schema for table <%.*s>: %d columns, pk <%.*s>\n", + table->len, table->s, sch->nr_cols, + sch->cols[sch->pk].name.len, sch->cols[sch->pk].name.s); + return sch; + +oom: + LM_ERR("no more pkg memory while loading schema for <%.*s>\n", + table->len, table->s); +error: + if (reply) + freeReplyObject(reply); + if (sch) { + if (sch->cols) { + for (c = 0; c < sch->nr_cols; c++) + if (sch->cols[c].name.s) + pkg_free(sch->cols[c].name.s); + pkg_free(sch->cols); + } + pkg_free(sch); + } + return NULL; +} + + +struct rdb_schema *rdb_get_schema(struct redis_con *con, const str *table) +{ + struct rdb_schema *sch; + + for (sch = con->schemas; sch; sch = sch->next) + if (sch->table.len == table->len && + memcmp(sch->table.s, table->s, table->len) == 0) + return sch; + + return rdb_load_schema(con, table); +} + + +int rdb_schema_col(const struct rdb_schema *sch, const str *name) +{ + int i; + + for (i = 0; i < sch->nr_cols; i++) + if (sch->cols[i].name.len == name->len && + memcmp(sch->cols[i].name.s, name->s, name->len) == 0) + return i; + return -1; +} + + +void rdb_free_schemas(struct redis_con *con) +{ + struct rdb_schema *sch, *next; + int c; + + for (sch = con->schemas; sch; sch = next) { + next = sch->next; + if (sch->cols) { + for (c = 0; c < sch->nr_cols; c++) + if (sch->cols[c].name.s) + pkg_free(sch->cols[c].name.s); + pkg_free(sch->cols); + } + pkg_free(sch); + } + con->schemas = NULL; +} diff --git a/modules/db_redis/schema.h b/modules/db_redis/schema.h new file mode 100644 index 00000000000..a8b31487f26 --- /dev/null +++ b/modules/db_redis/schema.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2026 OpenSIPS Solutions + * + * This file is part of opensips, a free SIP server. + * + * opensips is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version + * + * opensips is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef DB_REDIS_SCHEMA_H +#define DB_REDIS_SCHEMA_H + +#include "../../str.h" +#include "../../db/db_val.h" +#include "redis_con.h" + +/* a table schema is provisioned as the hash "schema:" with: + * __cols -> ordered, space-separated column names + * __pk -> name of the primary-key column + * -> type[,null][,auto] + * where type is one of: int, bigint, double, string, blob, datetime */ + +struct rdb_col { + str name; + db_type_t type; + int nullable; + int is_auto; /* auto-increment (emulated via INCR seq:
) */ +}; + +struct rdb_schema { + str table; + int nr_cols; + struct rdb_col *cols; + int pk; /* index into cols[] */ + struct rdb_schema *next; +}; + +/* cached lookup; loads "schema:
" from redis on first use */ +struct rdb_schema *rdb_get_schema(struct redis_con *con, const str *table); + +/* index of a column by name, -1 if not part of the schema */ +int rdb_schema_col(const struct rdb_schema *sch, const str *name); + +void rdb_free_schemas(struct redis_con *con); + +#endif /* DB_REDIS_SCHEMA_H */ diff --git a/modules/usrloc/dlist.c b/modules/usrloc/dlist.c index 3dd2db2b59d..5910d8672ba 100644 --- a/modules/usrloc/dlist.c +++ b/modules/usrloc/dlist.c @@ -129,6 +129,7 @@ static int get_domain_db_ucontacts(udomain_t *d, void *buf, int *len, unsigned int dbflags; int needed; int shortage = 0; + int client_side_partition = 0; uint64_t contact_id; void *record_start; @@ -149,51 +150,99 @@ static int get_domain_db_ucontacts(udomain_t *d, void *buf, int *len, goto error; } - i = snprintf(query_buf, sizeof query_buf, "select %.*s, %.*s, %.*s," + if (DB_CAPABILITY(ul_dbf, DB_CAP_RAW_QUERY)) { + /* the contact_id modulo partitioning is done in the query */ + i = snprintf(query_buf, sizeof query_buf, "select %.*s, %.*s, %.*s," #ifdef ORACLE_USRLOC " %.*s, %.*s, %.*s from %s where %.*s > %lld and mod(contact_id, %u) = %u", #else " %.*s, %.*s, %.*s from %s where %.*s > %lld and contact_id %% %u = %u", #endif - received_col.len, received_col.s, - contact_col.len, contact_col.s, - sock_col.len, sock_col.s, - cflags_col.len, cflags_col.s, - path_col.len, path_col.s, - contactid_col.len, contactid_col.s, - d->name->s, - expires_col.len, expires_col.s, - (long long)now, - part_max, part_idx); - - LM_DBG("query: %.*s\n", (int)(sizeof query_buf), query_buf); - if (i >= sizeof query_buf) { - LM_ERR("DB query too long\n"); - goto error; - } + received_col.len, received_col.s, + contact_col.len, contact_col.s, + sock_col.len, sock_col.s, + cflags_col.len, cflags_col.s, + path_col.len, path_col.s, + contactid_col.len, contactid_col.s, + d->name->s, + expires_col.len, expires_col.s, + (long long)now, + part_max, part_idx); + + LM_DBG("query: %.*s\n", (int)(sizeof query_buf), query_buf); + if (i >= sizeof query_buf) { + LM_ERR("DB query too long\n"); + goto error; + } + + query_str.s = query_buf; + query_str.len = i; + + if (DB_CAPABILITY(ul_dbf, DB_CAP_FETCH)) { + if (ul_dbf.raw_query(ul_dbh, &query_str, 0) < 0) { + LM_ERR("raw_query failed\n"); + goto error; + } - query_str.s = query_buf; - query_str.len = i; + no_rows = estimate_available_rows(20+128+20+128+64, 5); + if (no_rows == 0) + no_rows = 10; - if (DB_CAPABILITY(ul_dbf, DB_CAP_FETCH)) { - if (ul_dbf.raw_query(ul_dbh, &query_str, 0) < 0) { + LM_DBG("fetching %d rows\n", no_rows); + + if (ul_dbf.fetch_result(ul_dbh, &res, no_rows) < 0) { + LM_ERR("Error fetching rows\n"); + goto error; + } + } else if (ul_dbf.raw_query(ul_dbh, &query_str, &res) < 0) { LM_ERR("raw_query failed\n"); goto error; } + } else { + /* the DB backend has no raw query support (e.g. db_redis): run + * the equivalent structured query and apply the contact_id + * modulo partitioning while iterating the resulting rows */ + db_key_t cols[6]; + db_key_t keys[1]; + db_op_t ops[1]; + db_val_t vals[1]; + + cols[0] = &received_col; + cols[1] = &contact_col; + cols[2] = &sock_col; + cols[3] = &cflags_col; + cols[4] = &path_col; + cols[5] = &contactid_col; + + keys[0] = &expires_col; + ops[0] = OP_GT; + memset(vals, 0, sizeof vals); + VAL_TYPE(vals) = DB_BIGINT; + VAL_BIGINT(vals) = (long long)now; + + client_side_partition = (part_max > 1); + + if (DB_CAPABILITY(ul_dbf, DB_CAP_FETCH)) { + if (ul_dbf.query(ul_dbh, keys, ops, vals, cols, 1, 6, 0, 0) < 0) { + LM_ERR("query failed\n"); + goto error; + } - no_rows = estimate_available_rows(20+128+20+128+64, 5); - if (no_rows == 0) - no_rows = 10; + no_rows = estimate_available_rows(20+128+20+128+64, 5); + if (no_rows == 0) + no_rows = 10; - LM_DBG("fetching %d rows\n", no_rows); + LM_DBG("fetching %d rows\n", no_rows); - if (ul_dbf.fetch_result(ul_dbh, &res, no_rows) < 0) { - LM_ERR("Error fetching rows\n"); + if (ul_dbf.fetch_result(ul_dbh, &res, no_rows) < 0) { + LM_ERR("Error fetching rows\n"); + goto error; + } + } else if (ul_dbf.query(ul_dbh, keys, ops, vals, cols, + 1, 6, 0, &res) < 0) { + LM_ERR("query failed\n"); goto error; } - } else if (ul_dbf.raw_query(ul_dbh, &query_str, &res) < 0) { - LM_ERR("raw_query failed\n"); - goto error; } do { @@ -246,6 +295,12 @@ static int get_domain_db_ucontacts(udomain_t *d, void *buf, int *len, /* contact id*/ contact_id = VAL_BIGINT(ROW_VALUES(row) + 5); + /* when the query could not partition by contact_id (no raw + * query support), do the modulo partitioning here */ + if (client_side_partition && + contact_id % part_max != part_idx) + continue; + needed = (int)(p_len + sizeof p_len + r_len + sizeof r_len + p1_len + sizeof p1_len + sizeof sock + sizeof dbflags + sizeof next_hop); diff --git a/modules/usrloc/ul_mod.c b/modules/usrloc/ul_mod.c index 85445fdd12c..ab8ed8bc8f4 100644 --- a/modules/usrloc/ul_mod.c +++ b/modules/usrloc/ul_mod.c @@ -883,9 +883,10 @@ int ul_check_db(void) return -1; } + /* DB_CAP_RAW_QUERY is not required: contact loading has a + * structured-query fallback for backends without raw query + * support (e.g. db_redis) */ db_caps = DB_CAP_ALL; - if (cluster_mode == CM_SQL_ONLY) - db_caps |= DB_CAP_RAW_QUERY; if (!DB_CAPABILITY(ul_dbf, db_caps)) { LM_ERR("database module does not implement all functions" diff --git a/scripts/redis/README b/scripts/redis/README new file mode 100644 index 00000000000..c857e78f4b1 --- /dev/null +++ b/scripts/redis/README @@ -0,0 +1,50 @@ +db_redis provisioning files +=========================== + +These files provision table schemas for the db_redis module. Load the +ones for the modules you intend to back with Redis (add -c when talking +to a Redis Cluster): + + redis-cli [-c] < dialog-create.redis + +Note: redis-cli piped input does not support comment lines, so these +files contain commands only. + +Available files +--------------- + + dialog-create.redis - dialog module (dialog table, v12) + dispatcher-create.redis - dispatcher module (dispatcher table, v9) + usrloc-create.redis - usrloc module (location table, v1013) + clusterer-create.redis - clusterer module (clusterer table v4, + clusterer_bridge v1) + +Each file is self-contained: it also (re)creates the small "version" +table schema, so loading any file on its own is enough. + +Notes on specific modules: + + - The tm module keeps no database table and therefore has no schema + here. + +Data model +---------- + +A table schema is the hash "schema:
" holding: + + __cols -> ordered, space-separated column names + __pk -> primary-key column name + -> type[,null][,auto] + +where type is one of: int, bigint, double, string, blob, datetime. +The "auto" attribute marks an auto-increment primary key, emulated by +the module through INCR on "seq:
". + +Rows are stored as hashes at "
:", one field per +non-NULL column. The "version" table is provisioned like any other +table, with one row per served table, e.g.: + + HSET version:dialog table_name dialog table_version 12 + +The key prefixes "schema:" and "seq:" are reserved by the module and +must not be used as table names. diff --git a/scripts/redis/clusterer-create.redis b/scripts/redis/clusterer-create.redis new file mode 100644 index 00000000000..eee8f64bd52 --- /dev/null +++ b/scripts/redis/clusterer-create.redis @@ -0,0 +1,5 @@ +HSET schema:version __cols "table_name table_version" __pk table_name table_name string table_version int +HSET schema:clusterer __cols "id cluster_id node_id url state no_ping_retries priority sip_addr flags description" __pk id id int,auto cluster_id int node_id int url string state int no_ping_retries int priority int sip_addr string,null flags string,null description string,null +HSET version:clusterer table_name clusterer table_version 4 +HSET schema:clusterer_bridge __cols "id cluster_a cluster_b send_shtag dst_node_csv" __pk id id int,auto cluster_a int cluster_b int send_shtag string dst_node_csv string,null +HSET version:clusterer_bridge table_name clusterer_bridge table_version 1 diff --git a/scripts/redis/dialog-create.redis b/scripts/redis/dialog-create.redis new file mode 100644 index 00000000000..98d9e9ddfef --- /dev/null +++ b/scripts/redis/dialog-create.redis @@ -0,0 +1,3 @@ +HSET schema:version __cols "table_name table_version" __pk table_name table_name string table_version int +HSET schema:dialog __cols "dlg_id callid from_uri from_tag to_uri to_tag mangled_from_uri mangled_to_uri caller_cseq callee_cseq caller_ping_cseq callee_ping_cseq caller_route_set callee_route_set caller_contact callee_contact caller_sock callee_sock state start_time timeout vars profiles script_flags module_flags flags rt_on_answer rt_on_timeout rt_on_hangup" __pk dlg_id dlg_id bigint callid string from_uri string from_tag string to_uri string to_tag string mangled_from_uri string,null mangled_to_uri string,null caller_cseq string callee_cseq string caller_ping_cseq int callee_ping_cseq int caller_route_set string,null callee_route_set string,null caller_contact string,null callee_contact string,null caller_sock string callee_sock string state int start_time int timeout int vars blob,null profiles string,null script_flags string,null module_flags int flags int rt_on_answer string,null rt_on_timeout string,null rt_on_hangup string,null +HSET version:dialog table_name dialog table_version 12 diff --git a/scripts/redis/dispatcher-create.redis b/scripts/redis/dispatcher-create.redis new file mode 100644 index 00000000000..66960a17ebb --- /dev/null +++ b/scripts/redis/dispatcher-create.redis @@ -0,0 +1,3 @@ +HSET schema:version __cols "table_name table_version" __pk table_name table_name string table_version int +HSET schema:dispatcher __cols "id setid destination socket state probe_mode weight priority attrs description" __pk id id int,auto setid int destination string socket string,null state int probe_mode int weight string priority int attrs string,null description string,null +HSET version:dispatcher table_name dispatcher table_version 9 diff --git a/scripts/redis/usrloc-create.redis b/scripts/redis/usrloc-create.redis new file mode 100644 index 00000000000..da2a094c172 --- /dev/null +++ b/scripts/redis/usrloc-create.redis @@ -0,0 +1,3 @@ +HSET schema:version __cols "table_name table_version" __pk table_name table_name string table_version int +HSET schema:location __cols "contact_id username domain contact received path expires q callid cseq last_modified flags cflags user_agent socket methods sip_instance kv_store attr" __pk contact_id contact_id bigint,auto username string domain string,null contact string received string,null path string,null expires int q double callid string cseq int last_modified datetime flags int cflags string,null user_agent string socket string,null methods int,null sip_instance string,null kv_store string,null attr string,null +HSET version:location table_name location table_version 1013