You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

redis_pool.c 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. /*-
  2. * Copyright 2016 Vsevolod Stakhov
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #include "config.h"
  17. #include "contrib/libev/ev.h"
  18. #include "redis_pool.h"
  19. #include "cfg_file.h"
  20. #include "contrib/hiredis/hiredis.h"
  21. #include "contrib/hiredis/async.h"
  22. #include "contrib/hiredis/adapters/libev.h"
  23. #include "cryptobox.h"
  24. #include "logger.h"
  25. struct rspamd_redis_pool_elt;
  26. enum rspamd_redis_pool_connection_state {
  27. RSPAMD_REDIS_POOL_CONN_INACTIVE = 0,
  28. RSPAMD_REDIS_POOL_CONN_ACTIVE,
  29. RSPAMD_REDIS_POOL_CONN_FINALISING
  30. };
  31. struct rspamd_redis_pool_connection {
  32. struct redisAsyncContext *ctx;
  33. struct rspamd_redis_pool_elt *elt;
  34. GList *entry;
  35. ev_timer timeout;
  36. enum rspamd_redis_pool_connection_state state;
  37. gchar tag[MEMPOOL_UID_LEN];
  38. ref_entry_t ref;
  39. };
  40. struct rspamd_redis_pool_elt {
  41. struct rspamd_redis_pool *pool;
  42. guint64 key;
  43. GQueue *active;
  44. GQueue *inactive;
  45. };
  46. struct rspamd_redis_pool {
  47. struct ev_loop *event_loop;
  48. struct rspamd_config *cfg;
  49. GHashTable *elts_by_key;
  50. GHashTable *elts_by_ctx;
  51. gdouble timeout;
  52. guint max_conns;
  53. };
  54. static const gdouble default_timeout = 10.0;
  55. static const guint default_max_conns = 100;
  56. #define msg_err_rpool(...) rspamd_default_log_function (G_LOG_LEVEL_CRITICAL, \
  57. "redis_pool", conn->tag, \
  58. G_STRFUNC, \
  59. __VA_ARGS__)
  60. #define msg_warn_rpool(...) rspamd_default_log_function (G_LOG_LEVEL_WARNING, \
  61. "redis_pool", conn->tag, \
  62. G_STRFUNC, \
  63. __VA_ARGS__)
  64. #define msg_info_rpool(...) rspamd_default_log_function (G_LOG_LEVEL_INFO, \
  65. "redis_pool", conn->tag, \
  66. G_STRFUNC, \
  67. __VA_ARGS__)
  68. #define msg_debug_rpool(...) rspamd_conditional_debug_fast (NULL, NULL, \
  69. rspamd_redis_pool_log_id, "redis_pool", conn->tag, \
  70. G_STRFUNC, \
  71. __VA_ARGS__)
  72. INIT_LOG_MODULE(redis_pool)
  73. static inline guint64
  74. rspamd_redis_pool_get_key (const gchar *db, const gchar *password,
  75. const char *ip, int port)
  76. {
  77. rspamd_cryptobox_fast_hash_state_t st;
  78. rspamd_cryptobox_fast_hash_init (&st, rspamd_hash_seed ());
  79. if (db) {
  80. rspamd_cryptobox_fast_hash_update (&st, db, strlen (db));
  81. }
  82. if (password) {
  83. rspamd_cryptobox_fast_hash_update (&st, password, strlen (password));
  84. }
  85. rspamd_cryptobox_fast_hash_update (&st, ip, strlen (ip));
  86. rspamd_cryptobox_fast_hash_update (&st, &port, sizeof (port));
  87. return rspamd_cryptobox_fast_hash_final (&st);
  88. }
  89. static void
  90. rspamd_redis_pool_conn_dtor (struct rspamd_redis_pool_connection *conn)
  91. {
  92. if (conn->state == RSPAMD_REDIS_POOL_CONN_ACTIVE) {
  93. msg_debug_rpool ("active connection removed");
  94. if (conn->ctx) {
  95. if (!(conn->ctx->c.flags & REDIS_FREEING)) {
  96. redisAsyncContext *ac = conn->ctx;
  97. conn->ctx = NULL;
  98. g_hash_table_remove (conn->elt->pool->elts_by_ctx, ac);
  99. ac->onDisconnect = NULL;
  100. redisAsyncFree (ac);
  101. }
  102. }
  103. if (conn->entry) {
  104. g_queue_unlink (conn->elt->active, conn->entry);
  105. }
  106. }
  107. else {
  108. msg_debug_rpool ("inactive connection removed");
  109. ev_timer_stop (conn->elt->pool->event_loop, &conn->timeout);
  110. if (conn->ctx && !(conn->ctx->c.flags & REDIS_FREEING)) {
  111. redisAsyncContext *ac = conn->ctx;
  112. /* To prevent on_disconnect here */
  113. conn->state = RSPAMD_REDIS_POOL_CONN_FINALISING;
  114. g_hash_table_remove (conn->elt->pool->elts_by_ctx, ac);
  115. conn->ctx = NULL;
  116. ac->onDisconnect = NULL;
  117. redisAsyncFree (ac);
  118. }
  119. if (conn->entry) {
  120. g_queue_unlink (conn->elt->inactive, conn->entry);
  121. }
  122. }
  123. if (conn->entry) {
  124. g_list_free (conn->entry);
  125. }
  126. g_free (conn);
  127. }
  128. static void
  129. rspamd_redis_pool_elt_dtor (gpointer p)
  130. {
  131. GList *cur;
  132. struct rspamd_redis_pool_elt *elt = p;
  133. struct rspamd_redis_pool_connection *c;
  134. for (cur = elt->active->head; cur != NULL; cur = g_list_next (cur)) {
  135. c = cur->data;
  136. c->entry = NULL;
  137. REF_RELEASE (c);
  138. }
  139. for (cur = elt->inactive->head; cur != NULL; cur = g_list_next (cur)) {
  140. c = cur->data;
  141. c->entry = NULL;
  142. REF_RELEASE (c);
  143. }
  144. g_queue_free (elt->active);
  145. g_queue_free (elt->inactive);
  146. g_free (elt);
  147. }
  148. static void
  149. rspamd_redis_on_quit (redisAsyncContext *c, gpointer r, gpointer priv)
  150. {
  151. struct rspamd_redis_pool_connection *conn =
  152. (struct rspamd_redis_pool_connection *)priv;
  153. msg_debug_rpool ("quit command reply for the connection %p, refcount: %d",
  154. conn->ctx, conn->ref.refcount);
  155. /*
  156. * The connection will be freed by hiredis itself as we are here merely after
  157. * quit command has succeeded and we have timer being set already.
  158. * The problem is that when this callback is called, our connection is likely
  159. * dead, so probably even on_disconnect callback has been already called...
  160. *
  161. * Hence, the connection might already be freed, so even (conn) pointer may be
  162. * inaccessible.
  163. *
  164. * TODO: Use refcounts to prevent this stuff to happen, the problem is how
  165. * to handle Redis timeout on `quit` command in fact... The good thing is that
  166. * it will not likely happen.
  167. */
  168. }
  169. static void
  170. rspamd_redis_conn_timeout (EV_P_ ev_timer *w, int revents)
  171. {
  172. struct rspamd_redis_pool_connection *conn =
  173. (struct rspamd_redis_pool_connection *)w->data;
  174. g_assert (conn->state != RSPAMD_REDIS_POOL_CONN_ACTIVE);
  175. if (conn->state == RSPAMD_REDIS_POOL_CONN_INACTIVE) {
  176. msg_debug_rpool ("scheduled soft removal of connection %p, refcount: %d",
  177. conn->ctx, conn->ref.refcount);
  178. /* Prevent reusing */
  179. if (conn->entry) {
  180. g_queue_unlink (conn->elt->inactive, conn->entry);
  181. conn->entry = NULL;
  182. }
  183. conn->state = RSPAMD_REDIS_POOL_CONN_FINALISING;
  184. ev_timer_again (EV_A_ w);
  185. redisAsyncCommand (conn->ctx, rspamd_redis_on_quit, conn, "QUIT");
  186. }
  187. else {
  188. /* Finalising by timeout */
  189. ev_timer_stop (EV_A_ w);
  190. msg_debug_rpool ("final removal of connection %p, refcount: %d",
  191. conn->ctx, conn->ref.refcount);
  192. REF_RELEASE (conn);
  193. }
  194. }
  195. static void
  196. rspamd_redis_pool_schedule_timeout (struct rspamd_redis_pool_connection *conn)
  197. {
  198. gdouble real_timeout;
  199. guint active_elts;
  200. active_elts = g_queue_get_length (conn->elt->active);
  201. if (active_elts > conn->elt->pool->max_conns) {
  202. real_timeout = conn->elt->pool->timeout / 2.0;
  203. real_timeout = rspamd_time_jitter (real_timeout, real_timeout / 4.0);
  204. }
  205. else {
  206. real_timeout = conn->elt->pool->timeout;
  207. real_timeout = rspamd_time_jitter (real_timeout, real_timeout / 2.0);
  208. }
  209. msg_debug_rpool ("scheduled connection %p cleanup in %.1f seconds",
  210. conn->ctx, real_timeout);
  211. conn->timeout.data = conn;
  212. ev_timer_init (&conn->timeout,
  213. rspamd_redis_conn_timeout,
  214. real_timeout, real_timeout / 2.0);
  215. ev_timer_start (conn->elt->pool->event_loop, &conn->timeout);
  216. }
  217. static void
  218. rspamd_redis_pool_on_disconnect (const struct redisAsyncContext *ac, int status)
  219. {
  220. struct rspamd_redis_pool_connection *conn = ac->data;
  221. /*
  222. * Here, we know that redis itself will free this connection
  223. * so, we need to do something very clever about it
  224. */
  225. if (conn->state != RSPAMD_REDIS_POOL_CONN_ACTIVE) {
  226. /* Do nothing for active connections as it is already handled somewhere */
  227. if (conn->ctx) {
  228. msg_debug_rpool ("inactive connection terminated: %s, refs: %d",
  229. conn->ctx->errstr, conn->ref.refcount);
  230. }
  231. REF_RELEASE (conn);
  232. }
  233. }
  234. static struct rspamd_redis_pool_connection *
  235. rspamd_redis_pool_new_connection (struct rspamd_redis_pool *pool,
  236. struct rspamd_redis_pool_elt *elt,
  237. const char *db,
  238. const char *password,
  239. const char *ip,
  240. gint port)
  241. {
  242. struct rspamd_redis_pool_connection *conn;
  243. struct redisAsyncContext *ctx;
  244. if (*ip == '/' || *ip == '.') {
  245. ctx = redisAsyncConnectUnix (ip);
  246. }
  247. else {
  248. ctx = redisAsyncConnect (ip, port);
  249. }
  250. if (ctx) {
  251. if (ctx->err != REDIS_OK) {
  252. msg_err ("cannot connect to redis %s (port %d): %s", ip, port, ctx->errstr);
  253. redisAsyncFree (ctx);
  254. return NULL;
  255. }
  256. else {
  257. conn = g_malloc0 (sizeof (*conn));
  258. conn->entry = g_list_prepend (NULL, conn);
  259. conn->elt = elt;
  260. conn->state = RSPAMD_REDIS_POOL_CONN_ACTIVE;
  261. g_hash_table_insert (elt->pool->elts_by_ctx, ctx, conn);
  262. g_queue_push_head_link (elt->active, conn->entry);
  263. conn->ctx = ctx;
  264. ctx->data = conn;
  265. rspamd_random_hex (conn->tag, sizeof (conn->tag));
  266. REF_INIT_RETAIN (conn, rspamd_redis_pool_conn_dtor);
  267. msg_debug_rpool ("created new connection to %s:%d: %p", ip, port, ctx);
  268. redisLibevAttach (pool->event_loop, ctx);
  269. redisAsyncSetDisconnectCallback (ctx, rspamd_redis_pool_on_disconnect);
  270. if (password) {
  271. redisAsyncCommand (ctx, NULL, NULL,
  272. "AUTH %s", password);
  273. }
  274. if (db) {
  275. redisAsyncCommand (ctx, NULL, NULL,
  276. "SELECT %s", db);
  277. }
  278. }
  279. return conn;
  280. }
  281. return NULL;
  282. }
  283. static struct rspamd_redis_pool_elt *
  284. rspamd_redis_pool_new_elt (struct rspamd_redis_pool *pool)
  285. {
  286. struct rspamd_redis_pool_elt *elt;
  287. elt = g_malloc0 (sizeof (*elt));
  288. elt->active = g_queue_new ();
  289. elt->inactive = g_queue_new ();
  290. elt->pool = pool;
  291. return elt;
  292. }
  293. struct rspamd_redis_pool *
  294. rspamd_redis_pool_init (void)
  295. {
  296. struct rspamd_redis_pool *pool;
  297. pool = g_malloc0 (sizeof (*pool));
  298. pool->elts_by_key = g_hash_table_new_full (g_int64_hash, g_int64_equal,
  299. NULL, rspamd_redis_pool_elt_dtor);
  300. pool->elts_by_ctx = g_hash_table_new (g_direct_hash, g_direct_equal);
  301. return pool;
  302. }
  303. void
  304. rspamd_redis_pool_config (struct rspamd_redis_pool *pool,
  305. struct rspamd_config *cfg,
  306. struct ev_loop *ev_base)
  307. {
  308. g_assert (pool != NULL);
  309. pool->event_loop = ev_base;
  310. pool->cfg = cfg;
  311. pool->timeout = default_timeout;
  312. pool->max_conns = default_max_conns;
  313. }
  314. struct redisAsyncContext*
  315. rspamd_redis_pool_connect (struct rspamd_redis_pool *pool,
  316. const gchar *db, const gchar *password,
  317. const char *ip, int port)
  318. {
  319. guint64 key;
  320. struct rspamd_redis_pool_elt *elt;
  321. GList *conn_entry;
  322. struct rspamd_redis_pool_connection *conn;
  323. g_assert (pool != NULL);
  324. g_assert (pool->event_loop != NULL);
  325. g_assert (ip != NULL);
  326. key = rspamd_redis_pool_get_key (db, password, ip, port);
  327. elt = g_hash_table_lookup (pool->elts_by_key, &key);
  328. if (elt) {
  329. if (g_queue_get_length (elt->inactive) > 0) {
  330. conn_entry = g_queue_pop_head_link (elt->inactive);
  331. conn = conn_entry->data;
  332. g_assert (conn->state != RSPAMD_REDIS_POOL_CONN_ACTIVE);
  333. if (conn->ctx->err == REDIS_OK) {
  334. /* Also check SO_ERROR */
  335. gint err;
  336. socklen_t len = sizeof (gint);
  337. if (getsockopt (conn->ctx->c.fd, SOL_SOCKET, SO_ERROR,
  338. (void *) &err, &len) == -1) {
  339. err = errno;
  340. }
  341. if (err != 0) {
  342. g_list_free (conn->entry);
  343. conn->entry = NULL;
  344. REF_RELEASE (conn);
  345. conn = rspamd_redis_pool_new_connection (pool, elt,
  346. db, password, ip, port);
  347. }
  348. else {
  349. ev_timer_stop (elt->pool->event_loop, &conn->timeout);
  350. conn->state = RSPAMD_REDIS_POOL_CONN_ACTIVE;
  351. g_queue_push_tail_link (elt->active, conn_entry);
  352. msg_debug_rpool ("reused existing connection to %s:%d: %p",
  353. ip, port, conn->ctx);
  354. }
  355. }
  356. else {
  357. g_list_free (conn->entry);
  358. conn->entry = NULL;
  359. REF_RELEASE (conn);
  360. conn = rspamd_redis_pool_new_connection (pool, elt,
  361. db, password, ip, port);
  362. }
  363. }
  364. else {
  365. /* Need to create connection */
  366. conn = rspamd_redis_pool_new_connection (pool, elt,
  367. db, password, ip, port);
  368. }
  369. }
  370. else {
  371. /* Need to create a pool */
  372. elt = rspamd_redis_pool_new_elt (pool);
  373. elt->key = key;
  374. g_hash_table_insert (pool->elts_by_key, &elt->key, elt);
  375. conn = rspamd_redis_pool_new_connection (pool, elt,
  376. db, password, ip, port);
  377. }
  378. if (!conn) {
  379. return NULL;
  380. }
  381. REF_RETAIN (conn);
  382. return conn->ctx;
  383. }
  384. void
  385. rspamd_redis_pool_release_connection (struct rspamd_redis_pool *pool,
  386. struct redisAsyncContext *ctx, enum rspamd_redis_pool_release_type how)
  387. {
  388. struct rspamd_redis_pool_connection *conn;
  389. g_assert (pool != NULL);
  390. g_assert (ctx != NULL);
  391. conn = g_hash_table_lookup (pool->elts_by_ctx, ctx);
  392. if (conn != NULL) {
  393. g_assert (conn->state == RSPAMD_REDIS_POOL_CONN_ACTIVE);
  394. if (ctx->err != REDIS_OK) {
  395. /* We need to terminate connection forcefully */
  396. msg_debug_rpool ("closed connection %p due to an error", conn->ctx);
  397. REF_RELEASE (conn);
  398. }
  399. else {
  400. if (how == RSPAMD_REDIS_RELEASE_DEFAULT) {
  401. /* Ensure that there are no callbacks attached to this conn */
  402. if (ctx->replies.head == NULL) {
  403. /* Just move it to the inactive queue */
  404. g_queue_unlink (conn->elt->active, conn->entry);
  405. g_queue_push_head_link (conn->elt->inactive, conn->entry);
  406. conn->state = RSPAMD_REDIS_POOL_CONN_INACTIVE;
  407. rspamd_redis_pool_schedule_timeout (conn);
  408. msg_debug_rpool ("mark connection %p inactive", conn->ctx);
  409. }
  410. else {
  411. msg_debug_rpool ("closed connection %p due to callbacks left",
  412. conn->ctx);
  413. REF_RELEASE (conn);
  414. }
  415. }
  416. else {
  417. if (how == RSPAMD_REDIS_RELEASE_FATAL) {
  418. msg_debug_rpool ("closed connection %p due to an fatal termination",
  419. conn->ctx);
  420. }
  421. else {
  422. msg_debug_rpool ("closed connection %p due to explicit termination",
  423. conn->ctx);
  424. }
  425. REF_RELEASE (conn);
  426. }
  427. }
  428. REF_RELEASE (conn);
  429. }
  430. else {
  431. g_assert_not_reached ();
  432. }
  433. }
  434. void
  435. rspamd_redis_pool_destroy (struct rspamd_redis_pool *pool)
  436. {
  437. struct rspamd_redis_pool_elt *elt;
  438. GHashTableIter it;
  439. gpointer k, v;
  440. g_assert (pool != NULL);
  441. g_hash_table_iter_init (&it, pool->elts_by_key);
  442. while (g_hash_table_iter_next (&it, &k, &v)) {
  443. elt = v;
  444. rspamd_redis_pool_elt_dtor (elt);
  445. g_hash_table_iter_steal (&it);
  446. }
  447. g_hash_table_unref (pool->elts_by_ctx);
  448. g_hash_table_unref (pool->elts_by_key);
  449. g_free (pool);
  450. }
  451. const gchar*
  452. rspamd_redis_type_to_string (int type)
  453. {
  454. const gchar *ret = "unknown";
  455. switch (type) {
  456. case REDIS_REPLY_STRING:
  457. ret = "string";
  458. break;
  459. case REDIS_REPLY_ARRAY:
  460. ret = "array";
  461. break;
  462. case REDIS_REPLY_INTEGER:
  463. ret = "int";
  464. break;
  465. case REDIS_REPLY_STATUS:
  466. ret = "status";
  467. break;
  468. case REDIS_REPLY_NIL:
  469. ret = "nil";
  470. break;
  471. case REDIS_REPLY_ERROR:
  472. ret = "error";
  473. break;
  474. default:
  475. break;
  476. }
  477. return ret;
  478. }