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