anastasis

Credential backup and recovery protocol and service
Log | Files | Refs | Submodules | README | LICENSE

anastasis-db_pg.c (8841B)


      1 /*
      2   This file is part of Anastasis
      3   Copyright (C) 2020, 2021, 2022 Anastasis SARL
      4 
      5   Anastasis is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   Anastasis; see the file COPYING.GPL.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file stasis/anastasis-db_pg.c
     18  * @brief shared database state and helpers for the Anastasis postgres backend
     19  * @author Christian Grothoff
     20  * @author Marcello Stanisci
     21  */
     22 #include "platform.h"
     23 #include "anastasis-db_pg.h"
     24 #include "anastasis/anastasis-database/preflight.h"
     25 #include "anastasis/anastasis-database/transaction.h"
     26 #include "anastasis/anastasis-database/event.h"
     27 #include <taler/taler_pq_lib.h>
     28 
     29 
     30 /**
     31  * Prepared statements have been initialized in this edition.
     32  */
     33 uint64_t ANASTASIS_DB_prep_gen_;
     34 
     35 /**
     36  * Global database state.
     37  */
     38 struct PostgresClosure *pg;
     39 
     40 
     41 enum GNUNET_GenericReturnValue
     42 ANASTASIS_DB_start (const char *name)
     43 {
     44   struct GNUNET_PQ_ExecuteStatement es[] = {
     45     GNUNET_PQ_make_execute ("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
     46     GNUNET_PQ_EXECUTE_STATEMENT_END
     47   };
     48 
     49   GNUNET_break (GNUNET_OK ==
     50                 ANASTASIS_DB_preflight ());
     51   pg->transaction_name = name;
     52   if (GNUNET_OK !=
     53       GNUNET_PQ_exec_statements (pg->conn,
     54                                  es))
     55   {
     56     TALER_LOG_ERROR ("Failed to start transaction\n");
     57     GNUNET_break (0);
     58     return GNUNET_SYSERR;
     59   }
     60   return GNUNET_OK;
     61 }
     62 
     63 
     64 void
     65 ANASTASIS_DB_rollback (void)
     66 {
     67   struct GNUNET_PQ_ExecuteStatement es[] = {
     68     GNUNET_PQ_make_execute ("ROLLBACK"),
     69     GNUNET_PQ_EXECUTE_STATEMENT_END
     70   };
     71 
     72   if (GNUNET_OK !=
     73       GNUNET_PQ_exec_statements (pg->conn,
     74                                  es))
     75   {
     76     TALER_LOG_ERROR ("Failed to rollback transaction\n");
     77     GNUNET_break (0);
     78   }
     79   pg->transaction_name = NULL;
     80 }
     81 
     82 
     83 enum GNUNET_DB_QueryStatus
     84 ANASTASIS_DB_commit (void)
     85 {
     86   enum GNUNET_DB_QueryStatus qs;
     87   struct GNUNET_PQ_QueryParam no_params[] = {
     88     GNUNET_PQ_query_param_end
     89   };
     90 
     91   PREPARE ("do_commit",
     92            "COMMIT");
     93   {
     94     PGresult *result;
     95 
     96     result = GNUNET_PQ_exec_prepared (pg->conn,
     97                                       "do_commit",
     98                                       no_params);
     99     qs = GNUNET_PQ_eval_result (pg->conn,
    100                                 "do_commit",
    101                                 result);
    102     /* PostgreSQL accepts COMMIT on a transaction it has already aborted: it
    103        silently rolls the transaction back and answers PGRES_COMMAND_OK with the
    104        command tag ROLLBACK.  GNUNET_PQ_eval_result() cannot tell that apart from
    105        a real commit, so every caller took the success path for a transaction
    106        whose writes are gone.  Report it as a soft error instead: sessions run
    107        SERIALIZABLE, so the overwhelmingly likely cause is an unhandled
    108        40001/40P01 for which retrying is right; a caller with a retry loop gives
    109        up after MAX_RETRIES and fails visibly, one without fails immediately.
    110 
    111        The *success* path deliberately keeps returning exactly what
    112        GNUNET_PQ_eval_prepared_non_select() returned before.  That function is
    113        GNUNET_PQ_exec_prepared() + GNUNET_PQ_eval_result() plus a
    114        strtol(PQcmdTuples()) step that only runs when the status is already
    115        GNUNET_DB_STATUS_SUCCESS_NO_RESULTS, and PQcmdTuples() on a COMMIT command
    116        tag is the empty string -- so that step yields 0, which is
    117        GNUNET_DB_STATUS_SUCCESS_NO_RESULTS again. */
    118     if ( (0 <= qs) &&
    119          (NULL != result) &&
    120          (0 == strcmp ("ROLLBACK",
    121                        PQcmdStatus (result))) )
    122     {
    123       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    124                   "Transaction `%s' was rolled back by the database instead of committed; a previous statement must have failed without being handled\n",
    125                   pg->transaction_name);
    126       qs = GNUNET_DB_STATUS_SOFT_ERROR;
    127     }
    128     PQclear (result);
    129   }
    130   pg->transaction_name = NULL;
    131   return qs;
    132 }
    133 
    134 
    135 struct GNUNET_DB_EventHandler *
    136 ANASTASIS_DB_event_listen (const struct GNUNET_DB_EventHeaderP *es,
    137                            struct GNUNET_TIME_Relative timeout,
    138                            GNUNET_DB_EventCallback cb,
    139                            void *cb_cls)
    140 {
    141   return GNUNET_PQ_event_listen (pg->conn,
    142                                  es,
    143                                  timeout,
    144                                  cb,
    145                                  cb_cls);
    146 }
    147 
    148 
    149 void
    150 ANASTASIS_DB_event_listen_cancel (struct GNUNET_DB_EventHandler *eh)
    151 {
    152   GNUNET_PQ_event_listen_cancel (eh);
    153 }
    154 
    155 
    156 void
    157 ANASTASIS_DB_event_notify (const struct GNUNET_DB_EventHeaderP *es,
    158                            const void *extra,
    159                            size_t extra_size)
    160 {
    161   GNUNET_PQ_event_notify (pg->conn,
    162                           es,
    163                           extra,
    164                           extra_size);
    165 }
    166 
    167 
    168 /**
    169  * Function called each time we connect or reconnect to the
    170  * database. Gives the application a chance to run some
    171  * per-connection initialization logic.
    172  *
    173  * @param cls unused, NULL
    174  * @param pq database connection handle
    175  */
    176 static void
    177 reconnect_cb (void *cls,
    178               struct GNUNET_PQ_Context *pq)
    179 {
    180 #if AUTO_EXPLAIN
    181   /* Enable verbose logging to see where queries do not
    182      properly use indices */
    183   struct GNUNET_PQ_ExecuteStatement es[] = {
    184     GNUNET_PQ_make_try_execute ("LOAD 'auto_explain';"),
    185     GNUNET_PQ_make_try_execute ("SET auto_explain.log_min_duration=50;"),
    186     GNUNET_PQ_make_try_execute ("SET auto_explain.log_timing=TRUE;"),
    187     GNUNET_PQ_make_try_execute ("SET auto_explain.log_analyze=TRUE;"),
    188     /* https://wiki.postgresql.org/wiki/Serializable suggests to really
    189        force the default to 'serializable' if SSI is to be used. */
    190     GNUNET_PQ_make_try_execute (
    191       "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;"),
    192     GNUNET_PQ_make_try_execute ("SET enable_sort=OFF;"),
    193     GNUNET_PQ_make_try_execute ("SET enable_seqscan=OFF;"),
    194     GNUNET_PQ_make_execute ("SET search_path TO anastasis;"),
    195     GNUNET_PQ_EXECUTE_STATEMENT_END
    196   };
    197 #else
    198   struct GNUNET_PQ_ExecuteStatement es[] = {
    199     GNUNET_PQ_make_execute ("SET search_path TO anastasis;"),
    200     GNUNET_PQ_EXECUTE_STATEMENT_END
    201   };
    202 #endif
    203 
    204   (void) cls;
    205   if (GNUNET_OK !=
    206       GNUNET_PQ_exec_statements (pq,
    207                                  es))
    208   {
    209     GNUNET_break (0);
    210     return;
    211   }
    212   ANASTASIS_DB_prep_gen_++;
    213 }
    214 
    215 
    216 /**
    217  * Initialize the database connection.
    218  *
    219  * @param cfg configuration to use
    220  * @param check_current true to check if the database schema is current
    221  * @return NULL on failure
    222  */
    223 static enum GNUNET_GenericReturnValue
    224 do_init (const struct GNUNET_CONFIGURATION_Handle *cfg,
    225          bool check_current)
    226 {
    227   pg = GNUNET_new (struct PostgresClosure);
    228   pg->cfg = cfg;
    229   if (GNUNET_OK !=
    230       GNUNET_CONFIGURATION_get_value_string (cfg,
    231                                              "anastasis",
    232                                              "CURRENCY",
    233                                              &pg->currency))
    234   {
    235     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    236                                "anastasis",
    237                                "CURRENCY");
    238     GNUNET_free (pg);
    239     return GNUNET_SYSERR;
    240   }
    241   pg->conn = GNUNET_PQ_init (pg->cfg,
    242                              "stasis-postgres",
    243                              &reconnect_cb,
    244                              pg);
    245   if (NULL == pg->conn)
    246   {
    247     GNUNET_break (0);
    248     ANASTASIS_DB_fini ();
    249     return GNUNET_SYSERR;
    250   }
    251   if (check_current &&
    252       (GNUNET_OK !=
    253        GNUNET_PQ_check_current (pg->conn,
    254                                 "stasis-")) )
    255   {
    256     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    257                 "Database schema is not up-to-date. Try running anastasis-dbinit or anastasis-dbconfig!\n");
    258     ANASTASIS_DB_fini ();
    259     return GNUNET_NO;
    260   }
    261   return GNUNET_OK;
    262 }
    263 
    264 
    265 enum GNUNET_GenericReturnValue
    266 ANASTASIS_DB_init (const struct GNUNET_CONFIGURATION_Handle *cfg)
    267 {
    268   return do_init (cfg,
    269                   true);
    270 }
    271 
    272 
    273 enum GNUNET_GenericReturnValue
    274 ANASTASIS_DB_init_admin (const struct GNUNET_CONFIGURATION_Handle *cfg)
    275 {
    276   return do_init (cfg,
    277                   false);
    278 }
    279 
    280 
    281 void
    282 ANASTASIS_DB_fini (void)
    283 {
    284   if (NULL == pg)
    285     return;
    286   if (NULL != pg->conn)
    287     GNUNET_PQ_disconnect (pg->conn);
    288   GNUNET_free (pg->currency);
    289   GNUNET_free (pg);
    290 }
    291 
    292 
    293 /* end of anastasis-db_pg.c */