global reindent, now with uncrustify hook enabled
[oweals/gnunet.git] / src / datacache / plugin_datacache_postgres.c
1 /*
2      This file is part of GNUnet
3      Copyright (C) 2006, 2009, 2010, 2012, 2015, 2017, 2018 GNUnet e.V.
4
5      GNUnet is free software: you can redistribute it and/or modify it
6      under the terms of the GNU Affero General Public License as published
7      by the Free Software Foundation, either version 3 of the License,
8      or (at your option) any later version.
9
10      GNUnet is distributed in the hope that it will be useful, but
11      WITHOUT ANY WARRANTY; without even the implied warranty of
12      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13      Affero General Public License for more details.
14
15      You should have received a copy of the GNU Affero General Public License
16      along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18      SPDX-License-Identifier: AGPL3.0-or-later
19  */
20
21 /**
22  * @file datacache/plugin_datacache_postgres.c
23  * @brief postgres for an implementation of a database backend for the datacache
24  * @author Christian Grothoff
25  */
26 #include "platform.h"
27 #include "gnunet_util_lib.h"
28 #include "gnunet_pq_lib.h"
29 #include "gnunet_datacache_plugin.h"
30
31 #define LOG(kind, ...) GNUNET_log_from (kind, "datacache-postgres", __VA_ARGS__)
32
33 /**
34  * Per-entry overhead estimate
35  */
36 #define OVERHEAD (sizeof(struct GNUNET_HashCode) + 24)
37
38 /**
39  * Context for all functions in this plugin.
40  */
41 struct Plugin
42 {
43   /**
44    * Our execution environment.
45    */
46   struct GNUNET_DATACACHE_PluginEnvironment *env;
47
48   /**
49    * Native Postgres database handle.
50    */
51   PGconn *dbh;
52
53   /**
54    * Number of key-value pairs in the database.
55    */
56   unsigned int num_items;
57 };
58
59
60 /**
61  * @brief Get a database handle
62  *
63  * @param plugin global context
64  * @return #GNUNET_OK on success, #GNUNET_SYSERR on error
65  */
66 static int
67 init_connection (struct Plugin *plugin)
68 {
69   struct GNUNET_PQ_ExecuteStatement es[] = {
70     GNUNET_PQ_make_execute ("CREATE TEMPORARY TABLE IF NOT EXISTS gn011dc ("
71                             "  type INTEGER NOT NULL,"
72                             "  prox INTEGER NOT NULL,"
73                             "  discard_time BIGINT NOT NULL,"
74                             "  key BYTEA NOT NULL,"
75                             "  value BYTEA NOT NULL,"
76                             "  path BYTEA DEFAULT NULL)"
77                             "WITH OIDS"),
78     GNUNET_PQ_make_try_execute (
79       "CREATE INDEX IF NOT EXISTS idx_key ON gn011dc (key)"),
80     GNUNET_PQ_make_try_execute (
81       "CREATE INDEX IF NOT EXISTS idx_dt ON gn011dc (discard_time)"),
82     GNUNET_PQ_make_execute (
83       "ALTER TABLE gn011dc ALTER value SET STORAGE EXTERNAL"),
84     GNUNET_PQ_make_execute ("ALTER TABLE gn011dc ALTER key SET STORAGE PLAIN"),
85     GNUNET_PQ_EXECUTE_STATEMENT_END
86   };
87   struct GNUNET_PQ_PreparedStatement ps[] = {
88     GNUNET_PQ_make_prepare ("getkt",
89                             "SELECT discard_time,type,value,path FROM gn011dc "
90                             "WHERE key=$1 AND type=$2 AND discard_time >= $3",
91                             3),
92     GNUNET_PQ_make_prepare ("getk",
93                             "SELECT discard_time,type,value,path FROM gn011dc "
94                             "WHERE key=$1 AND discard_time >= $2",
95                             2),
96     GNUNET_PQ_make_prepare ("getex",
97                             "SELECT length(value) AS len,oid,key FROM gn011dc"
98                             " WHERE discard_time < $1"
99                             " ORDER BY discard_time ASC LIMIT 1",
100                             1),
101     GNUNET_PQ_make_prepare ("getm",
102                             "SELECT length(value) AS len,oid,key FROM gn011dc"
103                             " ORDER BY prox ASC, discard_time ASC LIMIT 1",
104                             0),
105     GNUNET_PQ_make_prepare ("get_random",
106                             "SELECT discard_time,type,value,path,key FROM gn011dc"
107                             " WHERE discard_time >= $1"
108                             " ORDER BY key ASC LIMIT 1 OFFSET $2",
109                             2),
110     GNUNET_PQ_make_prepare ("get_closest",
111                             "SELECT discard_time,type,value,path,key FROM gn011dc "
112                             "WHERE key>=$1 AND discard_time >= $2 ORDER BY key ASC LIMIT $3",
113                             3),
114     GNUNET_PQ_make_prepare ("delrow",
115                             "DELETE FROM gn011dc WHERE oid=$1",
116                             1),
117     GNUNET_PQ_make_prepare ("put",
118                             "INSERT INTO gn011dc (type, prox, discard_time, key, value, path) "
119                             "VALUES ($1, $2, $3, $4, $5, $6)",
120                             6),
121     GNUNET_PQ_PREPARED_STATEMENT_END
122   };
123
124   plugin->dbh = GNUNET_PQ_connect_with_cfg (plugin->env->cfg,
125                                             "datacache-postgres");
126   if (NULL == plugin->dbh)
127     return GNUNET_SYSERR;
128   if (GNUNET_OK !=
129       GNUNET_PQ_exec_statements (plugin->dbh,
130                                  es))
131   {
132     PQfinish (plugin->dbh);
133     plugin->dbh = NULL;
134     return GNUNET_SYSERR;
135   }
136
137   if (GNUNET_OK !=
138       GNUNET_PQ_prepare_statements (plugin->dbh,
139                                     ps))
140   {
141     PQfinish (plugin->dbh);
142     plugin->dbh = NULL;
143     return GNUNET_SYSERR;
144   }
145   return GNUNET_OK;
146 }
147
148
149 /**
150  * Store an item in the datastore.
151  *
152  * @param cls closure (our `struct Plugin`)
153  * @param key key to store @a data under
154  * @param prox proximity of @a key to my PID
155  * @param data_size number of bytes in @a data
156  * @param data data to store
157  * @param type type of the value
158  * @param discard_time when to discard the value in any case
159  * @param path_info_len number of entries in @a path_info
160  * @param path_info a path through the network
161  * @return 0 if duplicate, -1 on error, number of bytes used otherwise
162  */
163 static ssize_t
164 postgres_plugin_put (void *cls,
165                      const struct GNUNET_HashCode *key,
166                      uint32_t prox,
167                      size_t data_size,
168                      const char *data,
169                      enum GNUNET_BLOCK_Type type,
170                      struct GNUNET_TIME_Absolute discard_time,
171                      unsigned int path_info_len,
172                      const struct GNUNET_PeerIdentity *path_info)
173 {
174   struct Plugin *plugin = cls;
175   uint32_t type32 = (uint32_t) type;
176   struct GNUNET_PQ_QueryParam params[] = {
177     GNUNET_PQ_query_param_uint32 (&type32),
178     GNUNET_PQ_query_param_uint32 (&prox),
179     GNUNET_PQ_query_param_absolute_time (&discard_time),
180     GNUNET_PQ_query_param_auto_from_type (key),
181     GNUNET_PQ_query_param_fixed_size (data, data_size),
182     GNUNET_PQ_query_param_fixed_size (path_info,
183                                       path_info_len * sizeof(struct
184                                                              GNUNET_PeerIdentity)),
185     GNUNET_PQ_query_param_end
186   };
187   enum GNUNET_DB_QueryStatus ret;
188
189   ret = GNUNET_PQ_eval_prepared_non_select (plugin->dbh,
190                                             "put",
191                                             params);
192   if (0 > ret)
193     return -1;
194   plugin->num_items++;
195   return data_size + OVERHEAD;
196 }
197
198
199 /**
200  * Closure for #handle_results.
201  */
202 struct HandleResultContext
203 {
204   /**
205    * Function to call on each result, may be NULL.
206    */
207   GNUNET_DATACACHE_Iterator iter;
208
209   /**
210    * Closure for @e iter.
211    */
212   void *iter_cls;
213
214   /**
215    * Key used.
216    */
217   const struct GNUNET_HashCode *key;
218 };
219
220
221 /**
222  * Function to be called with the results of a SELECT statement
223  * that has returned @a num_results results.  Parse the result
224  * and call the callback given in @a cls
225  *
226  * @param cls closure of type `struct HandleResultContext`
227  * @param result the postgres result
228  * @param num_result the number of results in @a result
229  */
230 static void
231 handle_results (void *cls,
232                 PGresult *result,
233                 unsigned int num_results)
234 {
235   struct HandleResultContext *hrc = cls;
236
237   for (unsigned int i = 0; i < num_results; i++)
238   {
239     struct GNUNET_TIME_Absolute expiration_time;
240     uint32_t type;
241     void *data;
242     size_t data_size;
243     struct GNUNET_PeerIdentity *path;
244     size_t path_len;
245     struct GNUNET_PQ_ResultSpec rs[] = {
246       GNUNET_PQ_result_spec_absolute_time ("discard_time",
247                                            &expiration_time),
248       GNUNET_PQ_result_spec_uint32 ("type",
249                                     &type),
250       GNUNET_PQ_result_spec_variable_size ("value",
251                                            &data,
252                                            &data_size),
253       GNUNET_PQ_result_spec_variable_size ("path",
254                                            (void **) &path,
255                                            &path_len),
256       GNUNET_PQ_result_spec_end
257     };
258
259     if (GNUNET_YES !=
260         GNUNET_PQ_extract_result (result,
261                                   rs,
262                                   i))
263     {
264       GNUNET_break (0);
265       return;
266     }
267     if (0 != (path_len % sizeof(struct GNUNET_PeerIdentity)))
268     {
269       GNUNET_break (0);
270       path_len = 0;
271     }
272     path_len %= sizeof(struct GNUNET_PeerIdentity);
273     LOG (GNUNET_ERROR_TYPE_DEBUG,
274          "Found result of size %u bytes and type %u in database\n",
275          (unsigned int) data_size,
276          (unsigned int) type);
277     if ((NULL != hrc->iter) &&
278         (GNUNET_SYSERR ==
279          hrc->iter (hrc->iter_cls,
280                     hrc->key,
281                     data_size,
282                     data,
283                     (enum GNUNET_BLOCK_Type) type,
284                     expiration_time,
285                     path_len,
286                     path)))
287     {
288       LOG (GNUNET_ERROR_TYPE_DEBUG,
289            "Ending iteration (client error)\n");
290       GNUNET_PQ_cleanup_result (rs);
291       return;
292     }
293     GNUNET_PQ_cleanup_result (rs);
294   }
295 }
296
297
298 /**
299  * Iterate over the results for a particular key
300  * in the datastore.
301  *
302  * @param cls closure (our `struct Plugin`)
303  * @param key key to look for
304  * @param type entries of which type are relevant?
305  * @param iter maybe NULL (to just count)
306  * @param iter_cls closure for @a iter
307  * @return the number of results found
308  */
309 static unsigned int
310 postgres_plugin_get (void *cls,
311                      const struct GNUNET_HashCode *key,
312                      enum GNUNET_BLOCK_Type type,
313                      GNUNET_DATACACHE_Iterator iter,
314                      void *iter_cls)
315 {
316   struct Plugin *plugin = cls;
317   uint32_t type32 = (uint32_t) type;
318   struct GNUNET_TIME_Absolute now;
319   struct GNUNET_PQ_QueryParam paramk[] = {
320     GNUNET_PQ_query_param_auto_from_type (key),
321     GNUNET_PQ_query_param_absolute_time (&now),
322     GNUNET_PQ_query_param_end
323   };
324   struct GNUNET_PQ_QueryParam paramkt[] = {
325     GNUNET_PQ_query_param_auto_from_type (key),
326     GNUNET_PQ_query_param_uint32 (&type32),
327     GNUNET_PQ_query_param_absolute_time (&now),
328     GNUNET_PQ_query_param_end
329   };
330   enum GNUNET_DB_QueryStatus res;
331   struct HandleResultContext hr_ctx;
332
333   now = GNUNET_TIME_absolute_get ();
334   hr_ctx.iter = iter;
335   hr_ctx.iter_cls = iter_cls;
336   hr_ctx.key = key;
337   res = GNUNET_PQ_eval_prepared_multi_select (plugin->dbh,
338                                               (0 == type) ? "getk" : "getkt",
339                                               (0 == type) ? paramk : paramkt,
340                                               &handle_results,
341                                               &hr_ctx);
342   if (res < 0)
343     return 0;
344   return res;
345 }
346
347
348 /**
349  * Delete the entry with the lowest expiration value
350  * from the datacache right now.
351  *
352  * @param cls closure (our `struct Plugin`)
353  * @return #GNUNET_OK on success, #GNUNET_SYSERR on error
354  */
355 static int
356 postgres_plugin_del (void *cls)
357 {
358   struct Plugin *plugin = cls;
359   struct GNUNET_PQ_QueryParam pempty[] = {
360     GNUNET_PQ_query_param_end
361   };
362   uint32_t size;
363   uint32_t oid;
364   struct GNUNET_HashCode key;
365   struct GNUNET_PQ_ResultSpec rs[] = {
366     GNUNET_PQ_result_spec_uint32 ("len",
367                                   &size),
368     GNUNET_PQ_result_spec_uint32 ("oid",
369                                   &oid),
370     GNUNET_PQ_result_spec_auto_from_type ("key",
371                                           &key),
372     GNUNET_PQ_result_spec_end
373   };
374   enum GNUNET_DB_QueryStatus res;
375   struct GNUNET_PQ_QueryParam dparam[] = {
376     GNUNET_PQ_query_param_uint32 (&oid),
377     GNUNET_PQ_query_param_end
378   };
379   struct GNUNET_TIME_Absolute now;
380   struct GNUNET_PQ_QueryParam xparam[] = {
381     GNUNET_PQ_query_param_absolute_time (&now),
382     GNUNET_PQ_query_param_end
383   };
384
385   now = GNUNET_TIME_absolute_get ();
386   res = GNUNET_PQ_eval_prepared_singleton_select (plugin->dbh,
387                                                   "getex",
388                                                   xparam,
389                                                   rs);
390   if (0 >= res)
391     res = GNUNET_PQ_eval_prepared_singleton_select (plugin->dbh,
392                                                     "getm",
393                                                     pempty,
394                                                     rs);
395   if (0 > res)
396     return GNUNET_SYSERR;
397   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == res)
398   {
399     /* no result */
400     LOG (GNUNET_ERROR_TYPE_DEBUG,
401          "Ending iteration (no more results)\n");
402     return 0;
403   }
404   res = GNUNET_PQ_eval_prepared_non_select (plugin->dbh,
405                                             "delrow",
406                                             dparam);
407   if (0 > res)
408   {
409     GNUNET_PQ_cleanup_result (rs);
410     return GNUNET_SYSERR;
411   }
412   plugin->num_items--;
413   plugin->env->delete_notify (plugin->env->cls,
414                               &key,
415                               size + OVERHEAD);
416   GNUNET_PQ_cleanup_result (rs);
417   return GNUNET_OK;
418 }
419
420
421 /**
422  * Obtain a random key-value pair from the datacache.
423  *
424  * @param cls closure (our `struct Plugin`)
425  * @param iter maybe NULL (to just count)
426  * @param iter_cls closure for @a iter
427  * @return the number of results found, zero (datacache empty) or one
428  */
429 static unsigned int
430 postgres_plugin_get_random (void *cls,
431                             GNUNET_DATACACHE_Iterator iter,
432                             void *iter_cls)
433 {
434   struct Plugin *plugin = cls;
435   uint32_t off;
436   struct GNUNET_TIME_Absolute now;
437   struct GNUNET_TIME_Absolute expiration_time;
438   size_t data_size;
439   void *data;
440   size_t path_len;
441   struct GNUNET_PeerIdentity *path;
442   struct GNUNET_HashCode key;
443   uint32_t type;
444   enum GNUNET_DB_QueryStatus res;
445   struct GNUNET_PQ_QueryParam params[] = {
446     GNUNET_PQ_query_param_absolute_time (&now),
447     GNUNET_PQ_query_param_uint32 (&off),
448     GNUNET_PQ_query_param_end
449   };
450   struct GNUNET_PQ_ResultSpec rs[] = {
451     GNUNET_PQ_result_spec_absolute_time ("discard_time",
452                                          &expiration_time),
453     GNUNET_PQ_result_spec_uint32 ("type",
454                                   &type),
455     GNUNET_PQ_result_spec_variable_size ("value",
456                                          &data,
457                                          &data_size),
458     GNUNET_PQ_result_spec_variable_size ("path",
459                                          (void **) &path,
460                                          &path_len),
461     GNUNET_PQ_result_spec_auto_from_type ("key",
462                                           &key),
463     GNUNET_PQ_result_spec_end
464   };
465
466   if (0 == plugin->num_items)
467     return 0;
468   if (NULL == iter)
469     return 1;
470   now = GNUNET_TIME_absolute_get ();
471   off = GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_NONCE,
472                                   plugin->num_items);
473   res = GNUNET_PQ_eval_prepared_singleton_select (plugin->dbh,
474                                                   "get_random",
475                                                   params,
476                                                   rs);
477   if (0 > res)
478   {
479     GNUNET_break (0);
480     return 0;
481   }
482   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == res)
483   {
484     GNUNET_break (0);
485     return 0;
486   }
487   if (0 != (path_len % sizeof(struct GNUNET_PeerIdentity)))
488   {
489     GNUNET_break (0);
490     path_len = 0;
491   }
492   path_len %= sizeof(struct GNUNET_PeerIdentity);
493   LOG (GNUNET_ERROR_TYPE_DEBUG,
494        "Found random value with key %s of size %u bytes and type %u in database\n",
495        GNUNET_h2s (&key),
496        (unsigned int) data_size,
497        (unsigned int) type);
498   (void) iter (iter_cls,
499                &key,
500                data_size,
501                data,
502                (enum GNUNET_BLOCK_Type) type,
503                expiration_time,
504                path_len,
505                path);
506   GNUNET_PQ_cleanup_result (rs);
507   return 1;
508 }
509
510
511 /**
512  * Closure for #extract_result_cb.
513  */
514 struct ExtractResultContext
515 {
516   /**
517    * Function to call for each result found.
518    */
519   GNUNET_DATACACHE_Iterator iter;
520
521   /**
522    * Closure for @e iter.
523    */
524   void *iter_cls;
525 };
526
527
528 /**
529  * Function to be called with the results of a SELECT statement
530  * that has returned @a num_results results.  Calls the `iter`
531  * from @a cls for each result.
532  *
533  * @param cls closure with the `struct ExtractResultContext`
534  * @param result the postgres result
535  * @param num_result the number of results in @a result
536  */
537 static void
538 extract_result_cb (void *cls,
539                    PGresult *result,
540                    unsigned int num_results)
541 {
542   struct ExtractResultContext *erc = cls;
543
544   if (NULL == erc->iter)
545     return;
546   for (unsigned int i = 0; i < num_results; i++)
547   {
548     struct GNUNET_TIME_Absolute expiration_time;
549     uint32_t type;
550     void *data;
551     size_t data_size;
552     struct GNUNET_PeerIdentity *path;
553     size_t path_len;
554     struct GNUNET_HashCode key;
555     struct GNUNET_PQ_ResultSpec rs[] = {
556       GNUNET_PQ_result_spec_absolute_time ("",
557                                            &expiration_time),
558       GNUNET_PQ_result_spec_uint32 ("type",
559                                     &type),
560       GNUNET_PQ_result_spec_variable_size ("value",
561                                            &data,
562                                            &data_size),
563       GNUNET_PQ_result_spec_variable_size ("path",
564                                            (void **) &path,
565                                            &path_len),
566       GNUNET_PQ_result_spec_auto_from_type ("key",
567                                             &key),
568       GNUNET_PQ_result_spec_end
569     };
570
571     if (GNUNET_YES !=
572         GNUNET_PQ_extract_result (result,
573                                   rs,
574                                   i))
575     {
576       GNUNET_break (0);
577       return;
578     }
579     if (0 != (path_len % sizeof(struct GNUNET_PeerIdentity)))
580     {
581       GNUNET_break (0);
582       path_len = 0;
583     }
584     path_len %= sizeof(struct GNUNET_PeerIdentity);
585     LOG (GNUNET_ERROR_TYPE_DEBUG,
586          "Found result of size %u bytes and type %u in database\n",
587          (unsigned int) data_size,
588          (unsigned int) type);
589     if (GNUNET_SYSERR ==
590         erc->iter (erc->iter_cls,
591                    &key,
592                    data_size,
593                    data,
594                    (enum GNUNET_BLOCK_Type) type,
595                    expiration_time,
596                    path_len,
597                    path))
598     {
599       LOG (GNUNET_ERROR_TYPE_DEBUG,
600            "Ending iteration (client error)\n");
601       GNUNET_PQ_cleanup_result (rs);
602       break;
603     }
604     GNUNET_PQ_cleanup_result (rs);
605   }
606 }
607
608
609 /**
610  * Iterate over the results that are "close" to a particular key in
611  * the datacache.  "close" is defined as numerically larger than @a
612  * key (when interpreted as a circular address space), with small
613  * distance.
614  *
615  * @param cls closure (internal context for the plugin)
616  * @param key area of the keyspace to look into
617  * @param num_results number of results that should be returned to @a iter
618  * @param iter maybe NULL (to just count)
619  * @param iter_cls closure for @a iter
620  * @return the number of results found
621  */
622 static unsigned int
623 postgres_plugin_get_closest (void *cls,
624                              const struct GNUNET_HashCode *key,
625                              unsigned int num_results,
626                              GNUNET_DATACACHE_Iterator iter,
627                              void *iter_cls)
628 {
629   struct Plugin *plugin = cls;
630   uint32_t num_results32 = (uint32_t) num_results;
631   struct GNUNET_TIME_Absolute now;
632   struct GNUNET_PQ_QueryParam params[] = {
633     GNUNET_PQ_query_param_auto_from_type (key),
634     GNUNET_PQ_query_param_absolute_time (&now),
635     GNUNET_PQ_query_param_uint32 (&num_results32),
636     GNUNET_PQ_query_param_end
637   };
638   enum GNUNET_DB_QueryStatus res;
639   struct ExtractResultContext erc;
640
641   erc.iter = iter;
642   erc.iter_cls = iter_cls;
643   now = GNUNET_TIME_absolute_get ();
644   res = GNUNET_PQ_eval_prepared_multi_select (plugin->dbh,
645                                               "get_closest",
646                                               params,
647                                               &extract_result_cb,
648                                               &erc);
649   if (0 > res)
650   {
651     LOG (GNUNET_ERROR_TYPE_DEBUG,
652          "Ending iteration (postgres error)\n");
653     return 0;
654   }
655   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == res)
656   {
657     /* no result */
658     LOG (GNUNET_ERROR_TYPE_DEBUG,
659          "Ending iteration (no more results)\n");
660     return 0;
661   }
662   return res;
663 }
664
665
666 /**
667  * Entry point for the plugin.
668  *
669  * @param cls closure (the `struct GNUNET_DATACACHE_PluginEnvironmnet`)
670  * @return the plugin's closure (our `struct Plugin`)
671  */
672 void *
673 libgnunet_plugin_datacache_postgres_init (void *cls)
674 {
675   struct GNUNET_DATACACHE_PluginEnvironment *env = cls;
676   struct GNUNET_DATACACHE_PluginFunctions *api;
677   struct Plugin *plugin;
678
679   plugin = GNUNET_new (struct Plugin);
680   plugin->env = env;
681
682   if (GNUNET_OK != init_connection (plugin))
683   {
684     GNUNET_free (plugin);
685     return NULL;
686   }
687
688   api = GNUNET_new (struct GNUNET_DATACACHE_PluginFunctions);
689   api->cls = plugin;
690   api->get = &postgres_plugin_get;
691   api->put = &postgres_plugin_put;
692   api->del = &postgres_plugin_del;
693   api->get_random = &postgres_plugin_get_random;
694   api->get_closest = &postgres_plugin_get_closest;
695   LOG (GNUNET_ERROR_TYPE_INFO,
696        "Postgres datacache running\n");
697   return api;
698 }
699
700
701 /**
702  * Exit point from the plugin.
703  *
704  * @param cls closure (our `struct Plugin`)
705  * @return NULL
706  */
707 void *
708 libgnunet_plugin_datacache_postgres_done (void *cls)
709 {
710   struct GNUNET_DATACACHE_PluginFunctions *api = cls;
711   struct Plugin *plugin = api->cls;
712
713   PQfinish (plugin->dbh);
714   GNUNET_free (plugin);
715   GNUNET_free (api);
716   return NULL;
717 }
718
719
720
721 /* end of plugin_datacache_postgres.c */