summaryrefslogtreecommitdiffabout
authorSergey Poznyakoff <gray@nxc.no>2017-08-17 11:18:56 (GMT)
committer Sergey Poznyakoff <gray@nxc.no>2017-08-17 12:22:27 (GMT)
commitf25c77a5f9af8e0c4eb40dfae1683f383104ed3a (patch) (side-by-side diff)
treea727e78cbfb21c617d73b327262bcae645504466
parent168d61f49a4ec1dc2a956e848000d970dffa017f (diff)
downloadnssync-f25c77a5f9af8e0c4eb40dfae1683f383104ed3a.tar.gz
nssync-f25c77a5f9af8e0c4eb40dfae1683f383104ed3a.tar.bz2
Implement asynchronous and periodic request handling in server mode.
In server mode, the server wakes up periodically to run synchronizations. The wake up interval is configured by the server.wakeup statement. By default, POST requests to /nssync endpoint are delayed for certain time. This avoids spurious wake-ups if several requests come one after another during a short time. The delay is configured by the server.quarantine statement. Finally, the GET request returns the latest wakeup status. * src/Makefile.am [COND_MICROHTTPD] (PTHREAD_L): Add -lrt [COND_MICROHTTPD] (nssync_SOURCES): Add timer.c * src/config.c (server_kw): New keywords: "wakeup" and "delay". * src/nssync.c (periodic_timeout) (delay_timeout): New globals. (nssync): Add timestamp to the response. * src/nssync.h (periodic_timeout) (delay_timeout): New externs. (nssync_reschedule,nssync_timer): (nssync_format_result): New protos. * src/server.c (do_sync): Parse POST body. If present, it must be a valid JSON object. Handle GET requests (return most recent status). (nssync_server): Start nssync_timer thread. * src/timer.c: New file. * NEWS: Update. * doc/nssync.texi: Update. * doc/nssync.8: Update.
Diffstat (more/less context) (ignore whitespace changes)
-rw-r--r--NEWS22
-rw-r--r--doc/nssync.829
-rw-r--r--doc/nssync.texi66
-rw-r--r--src/Makefile.am4
-rw-r--r--src/cmdline.opt6
-rw-r--r--src/config.c8
-rw-r--r--src/nssync.c13
-rw-r--r--src/nssync.h7
-rw-r--r--src/server.c98
-rw-r--r--src/timer.c143
10 files changed, 353 insertions, 43 deletions
diff --git a/NEWS b/NEWS
index 2fd9acd..b35e6a8 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,4 @@
-NSsync NEWS -- history of user-visible changes. 2017-08-16
+NSsync NEWS -- history of user-visible changes. 2017-08-17
Copyright (C) 2011-2017 Sergey Poznyakoff
See the end of file for copying conditions.
@@ -14,6 +14,26 @@ preconfigured address and port (by default, 127.0.0.1:8080) for REST
HTTP requests. A POST request to the URI `/nssync' causes the program
to wake up and perform synchronization.
+While listening for requests, the server will wake up periodically
+to run scheduled synchronizations. Thus, this mode combines periodic
+and on-demand synchronizations.
+
+The server mode settings are supplied in the `server' configuration
+block statement:
+
+ server {
+ # Configures IP and port to listen on.
+ address [IP][:PORT];
+ # Configures the on-demand sync delay, in seconds. The server
+ # will wait this number of seconds after receiving the latest
+ # synch request and actually performing the synchronization.
+ # This avoids spurious wake-ups which would result if several
+ # requests arrive in sequence during a short interval.
+ delay N;
+ # Configures the periodic sync interval, in seconds.
+ wakeup N;
+ }
+
* check-ns
This new configuration statement controls which zones are served by
diff --git a/doc/nssync.8 b/doc/nssync.8
index 26d67d5..b7db8c1 100644
--- a/doc/nssync.8
+++ b/doc/nssync.8
@@ -13,7 +13,7 @@
.\"
.\" You should have received a copy of the GNU General Public License
.\" along with Nssync. If not, see <http://www.gnu.org/licenses/>.
-.TH NSSYNC "8" "August 16, 2017" "NSSYNC" ""
+.TH NSSYNC "8" "August 17, 2017" "NSSYNC" ""
.SH NAME
\fBnssync\fR \- A DNS Zone File Maintenance Utility
@@ -25,6 +25,7 @@
[\fB\-c\fR \fIFILE\fR]\
[\fB\-\-config\-file=\fIFILE\fR]\
[\fB\-\-config\-help\fR]\
+ [\fB\-\-cron\fR]\
[\fB\-\-debug\fR]\
[\fB\-\-debug\-lexer\fR]\
[\fB\-\-debug\-parser\fR]\
@@ -69,10 +70,10 @@ It does so by polling the database to determine records that have
recently changed and converting the database into BIND zone
files.
-The program can be run either periodically, as a cron job, or as a
-standalone daemon. In the latter case, it listens on a specified
-address for a specific HTTP request and triggers zone updates upon
-receiving it.
+The program can be run as a cron job, or as a standalone daemon. In
+the latter case, it listens on a specified address for a specific HTTP
+request and triggers zone updates upon receiving it. While listening,
+it will also wake up periodically to run unconditional synchronizations.
.SH OPTIONS
.TP
\fB\-E\fR
@@ -81,6 +82,9 @@ Preprocess configuration file and exit.
\fB\-c\fR \fIFILE\fR, \fB\-\-config\-file=\fIFILE\fR
Use \fIFILE\fR instead of the default configuration file.
.TP
+\fB\-\-cron\fR
+Operate in cron mode. This is the default.
+.TP
\fB\-f\fR, \fB\-\-force\fR
Proceed even if SQL slave status has not changed.
.TP
@@ -211,6 +215,8 @@ of the group
.EX
server {
address [\fIIP\fR][fB:\fIPORT\fR];
+ delay \fISECONDS\fR;
+ wakeup \fISECONDS\fR;
}
.EE
.TP
@@ -220,6 +226,19 @@ Either IP or port parts can be omitted, but not both. In any case, the
colon before \fIPORT\fR is mandatory. A host name can be used instead
of numeric IP, and a symbolic service name (from \fB/etc/services\fR),
in place of the numeric port.
+.TP
+\fBwakeup\fR \fISECONDS\fR;
+Configures a periodic wake-up interval, in seconds. This allows you
+to run synchronizations periodically, while being able to perform them
+on request. The default wake-up interval is 3600 seconds.
+.TP
+\fBdelay\fR \fISECONDS\fR;
+Configures the on-demand synchronization delay. Upon receiving a
+synchronization request, the server will wait this number of seconds
+before actually satisfying it. If another request arrives during this
+interval, the prior one will be cancelled and the request rescheduled.
+This helps avoid lots of spurious wake-ups that would otherwise occur
+in case if several requests arrive in series within a short interval.
.SS Syslog configuration
.EX
syslog {
diff --git a/doc/nssync.texi b/doc/nssync.texi
index 59b6863..22852c7 100644
--- a/doc/nssync.texi
+++ b/doc/nssync.texi
@@ -190,14 +190,43 @@ configuration file (@pxref{syslog statement}).
@node Server mode
@section Server Mode
Server mode is enabled by the @option{--server} command line option.
+It combines both periodic and on-demain synchronization features.
In this mode, @command{nssync} switches to background mode (becomes a
@dfn{daemon}) and starts listening on the preconfigured address and
-port (by default, 127.0.0.1:8080) for REST HTTP requests. A
-@samp{POST} request to the URI @samp{/nssync} causes the program to
-wake up and perform synchronization. The response contains a JSON
-object, containing at least one boolean key: @code{success}. Its
-value is @code{true} if the operation terminated successfully and
-@code{false} otherwise. Other possible keys are:
+port (by default, 127.0.0.1:8080) for REST HTTP requests. While
+listening, the server wakes up periodically to run the synchronization
+procedure. The wake-up interval is configured using the
+@samp{server.wakeup} statement (@pxref{server.wakeup}). Default value
+is 3600 (1 hour).
+
+The REST API provides a single endpoint: @samp{/nssync}. Two methods
+are supported:
+
+@table @asis
+@item POST
+A @samp{POST} request to the URI @samp{/nssync} schedules the
+synchronization. The configuration statement @samp{server.delay}
+configures the wakeup delay in seconds. When this number of seconds
+expires and no other POST request is received, the server will wake up
+and run the synchronization. Default delay is 30 seconds.
+
+If the @samp{application/json} body is present, it must be a valid
+JSON object with one boolean key: @samp{wait}. If it is @samp{true}
+the server will wake up immediately.
+
+The response contains a JSON object, containing at least the following
+two keys:
+
+@table @samp
+@item success
+A boolean value. Its value is @code{true} if the operation terminated
+successfully and @code{false} otherwise.
+
+@item timestamp
+Time when the synchronization finished (UNIX timestamp, GMT).
+@end table
+
+Other possible keys are:
@table @samp
@item zones
@@ -218,6 +247,11 @@ if the error is a general one.
@end table
@end table
+@item GET
+Returns the status of the last synchronization. See above for the
+format.
+@end table
+
In server mode, all diagnostics is reported via syslog.
@node Configuration File
@@ -353,7 +387,8 @@ listening on IP address 127.0.0.1, port 8080 and will use
change the address, use the @code{server} block statement.
@deffn {Configuration} server
-The server block can contain only one keyword: @code{address}.
+The server block can contain the following keywords: @code{address},
+@code{wakeup}, and @code{delay}.
@end deffn
@deffn {server} address [@var{IP}][:@var{PORT}]
@@ -370,6 +405,20 @@ server @{
@end example
@end deffn
+@anchor{server.wakeup}
+@deffn {server} wakeup @var{N}
+Specifies periodic synchronization interval in seconds. The server
+will wake up each @var{N} seconds to run a mandatory synchronization.
+The default interval is 3600 seconds.
+@end deffn
+
+@anchor{server.delay}
+@deffn{server} delay @var{N}
+Configures the on-demand synchronization delay. The server will wait
+@var{N} seconds after receiving a synchronization request
+(@pxref{Server mode, POST}). Default value is 30 seconds.
+@end deffn
+
@anchor{syslog statement}
@deffn {Configuration} syslog
This block statement configures logging via @command{syslog}. It can
@@ -613,6 +662,9 @@ Preprocess configuration file and exit.
@itemx --config-file=@var{file}
Use @var{file} instead of the default configuration file.
+@item --cron
+Operate in cron mode (@pxref{Cron mode}). This is the default.
+
@item -f
@itemx --force
Proceed even if slave status has not changed (@pxref{slave-status-file}).
diff --git a/src/Makefile.am b/src/Makefile.am
index 4c36c9f..0397119 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -26,8 +26,8 @@ nssync_SOURCES = \
sqlop.c
if COND_MICROHTTPD
- nssync_SOURCES += server.c
- PTHREAD_L=-lpthread
+ nssync_SOURCES += server.c timer.c
+ PTHREAD_L=-lpthread -lrt
else
nssync_SOURCES += noserver.c
PTHREAD_L=
diff --git a/src/cmdline.opt b/src/cmdline.opt
index 9f27cb4..221133e 100644
--- a/src/cmdline.opt
+++ b/src/cmdline.opt
@@ -61,6 +61,12 @@ BEGIN
config_file = optarg;
END
+OPTION(cron,,,
+ [<run as a periodic cron job (default)>])
+BEGIN
+ server_mode = 0;
+END
+
OPTION(server,s,,
[<run as a server>])
BEGIN
diff --git a/src/config.c b/src/config.c
index cc05404..5ff9280 100644
--- a/src/config.c
+++ b/src/config.c
@@ -25,6 +25,14 @@ static struct grecs_keyword server_kw[] = {
NULL, "Listen on that address",
grecs_type_sockaddr, GRECS_DFLT,
&server_addr },
+ { "wakeup",
+ "N", "Wake up and resync each N seconds",
+ grecs_type_uint, GRECS_DFLT,
+ &periodic_timeout },
+ { "delay",
+ "N", "Wait N seconds before actually performing the requested sync",
+ grecs_type_uint, GRECS_DFLT,
+ &delay_timeout },
{ NULL }
};
diff --git a/src/nssync.c b/src/nssync.c
index 93fa7fb..d10ce05 100644
--- a/src/nssync.c
+++ b/src/nssync.c
@@ -19,6 +19,7 @@
#include <fcntl.h>
#include "runcap.h"
#include <sys/types.h>
+#include <sys/time.h>
#include <signal.h>
#include <syslog.h>
@@ -37,6 +38,9 @@ struct json_value *changed_zones;
int check_ns;
int server_mode;
struct grecs_sockaddr *server_addr;
+unsigned periodic_timeout = 3600;
+unsigned delay_timeout = 30;
+
int syslog_facility = LOG_DAEMON;
char *syslog_tag;
int foreground;
@@ -607,12 +611,19 @@ nssync(struct json_value **ret_json)
}
if (ret_json) {
- struct json_value *json = json_new_object();
+ struct timeval tv;
+ struct timezone tz;
+ struct json_value *json;
+
+ json = json_new_object();
json_object_set(json, "success", json_new_bool(!res));
if (json_array_size(changed_zones) > 0)
json_object_set(json, "zones", changed_zones);
if (json_array_size(error_list) > 0)
json_object_set(json, "errors", error_list);
+ gettimeofday(&tv, &tz);
+ tv.tv_sec += tz.tz_minuteswest * 60;
+ json_object_set(json, "timestamp", json_new_number(tv.tv_sec));
*ret_json = json;
} else {
json_value_free(error_list);
diff --git a/src/nssync.h b/src/nssync.h
index 27baf23..e8150ac 100644
--- a/src/nssync.h
+++ b/src/nssync.h
@@ -51,6 +51,8 @@ extern struct grecs_sockaddr *server_addr;
#ifndef DEFAULT_NSSYNC_ADDR
# define DEFAULT_NSSYNC_ADDR "127.0.0.1:8080"
#endif
+extern unsigned periodic_timeout;
+extern unsigned delay_timeout;
extern char *sql_config_file;
extern char *sql_config_group;
@@ -151,7 +153,12 @@ int move_file(const char *file, const char *dst_file);
int copy_file(const char *file_name, FILE *infile, const char *dst_file);
void filetab_clear(void);
+
int nssync_server(void);
+void nssync_reschedule(int);
+void *nssync_timer(void *arg);
+char *nssync_format_result(void);
+
int nssync(struct json_value **ret_json);
diff --git a/src/server.c b/src/server.c
index f326547..a0b0312 100644
--- a/src/server.c
+++ b/src/server.c
@@ -112,6 +112,22 @@ cb_not_found(void *cls,
MHD_destroy_response(response);
return ret;
}
+
+static int
+bad_request(struct MHD_Connection *connection)
+{
+ int ret;
+ struct MHD_Response *response;
+ static char errtext[] = "bad request";
+
+ response = MHD_create_response_from_buffer (strlen(errtext),
+ (void *) errtext,
+ MHD_RESPMEM_PERSISTENT);
+ ret = MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, response);
+ MHD_destroy_response(response);
+ return ret;
+}
+
static int
cb_bad_method(void *cls,
@@ -132,13 +148,6 @@ cb_bad_method(void *cls,
return ret;
}
-static void
-json_writer(void *closure, char const *text, size_t len)
-{
- grecs_txtacc_t acc = closure;
- grecs_txtacc_grow(acc, text, len);
-}
-
static int
do_sync(void *cls,
struct MHD_Connection *connection,
@@ -147,37 +156,69 @@ do_sync(void *cls,
const char *upload_data, size_t *upload_data_size,
void **con_cls)
{
- int ret, rc;
+ int ret;
struct MHD_Response *response;
- struct json_value *json;
- struct json_format fmt;
- grecs_txtacc_t acc;
char *buf;
+ grecs_txtacc_t acc = *con_cls;
if (*url)
return cb_not_found(cls, connection, url, method,
version, upload_data, upload_data_size,
con_cls);
- if (strcmp(method, "POST"))
+ if (strcmp(method, MHD_HTTP_METHOD_GET) == 0) {
+ /* Nothing */;
+ } else if (strcmp(method, MHD_HTTP_METHOD_POST) == 0) {
+ if (!acc) {
+ char const *t = MHD_lookup_connection_value(connection,
+ MHD_HEADER_KIND,
+ MHD_HTTP_HEADER_CONTENT_TYPE);
+ if (t && strcmp(t, "application/json"))
+ return bad_request(connection);
+ acc = grecs_txtacc_create();
+ *con_cls = acc;
+ return MHD_YES;
+ }
+ if (*upload_data_size) {
+ grecs_txtacc_grow(acc, upload_data, *upload_data_size);
+ *upload_data_size = 0;
+ return MHD_YES;
+ } else {
+ /* Done with the data, serve the request now */
+ char *p;
+ struct json_value *json;
+
+ grecs_txtacc_grow_char(acc, 0);
+ p = grecs_txtacc_finish(acc, 0);
+
+ if (*p) {
+ json = json_parse_string(p, strlen(p));
+ } else
+ json = json_new_null();
+
+ grecs_txtacc_free(acc);
+ con_cls = NULL;
+
+ if (!json)
+ return bad_request(connection);
+
+ if (json->type == json_null) {
+ nssync_reschedule(0);
+ } else if (json->type == json_object) {
+ struct json_value *v;
+ if (json_object_get(json, "wait", &v) == 0
+ && v->type == json_bool) {
+ nssync_reschedule(v->v.b);
+ }
+ }
+ json_value_free(json);
+ }
+ } else
return cb_bad_method(cls, connection, url, method,
version, upload_data, upload_data_size,
con_cls);
-
- rc = nssync(&json);
- acc = grecs_txtacc_create();
- fmt.indent = 0;
- fmt.precision = 0;
- fmt.write = json_writer;
- fmt.data = acc;
- json_format_value(json, &fmt);
- json_value_free(json);
-
- grecs_txtacc_grow_char(acc, 0);
- buf = grecs_txtacc_finish(acc, 1);
- grecs_txtacc_free(acc);
-
+ buf = nssync_format_result();
response = MHD_create_response_from_buffer (strlen(buf),
(void *) buf,
MHD_RESPMEM_MUST_FREE);
@@ -187,7 +228,6 @@ do_sync(void *cls,
MHD_destroy_response(response);
return ret;
}
-
struct nssync_resource {
char const *uri;
@@ -251,6 +291,7 @@ nssync_server(void)
sigset_t sigs;
int i;
int sn;
+ pthread_t thr;
if (!server_addr) {
if (grecs_str_to_sockaddr(&server_addr, DEFAULT_NSSYNC_ADDR,
@@ -292,6 +333,9 @@ nssync_server(void)
MHD_OPTION_LISTEN_SOCKET, fd,
MHD_OPTION_EXTERNAL_LOGGER, nssync_mhd_logger, NULL,
MHD_OPTION_END);
+
+ pthread_create(&thr, NULL, nssync_timer, NULL);
+
/* Unblock only the fatal signals */
sigdelset(&sigs, SIGPIPE);
pthread_sigmask(SIG_UNBLOCK, &sigs, NULL);
diff --git a/src/timer.c b/src/timer.c
new file mode 100644
index 0000000..e0f0928
--- a/dev/null
+++ b/src/timer.c
@@ -0,0 +1,143 @@
+/* This file is part of NSsync
+ Copyright (C) 2017 Sergey Poznyakoff
+
+ NSsync 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 3, or (at your option)
+ any later version.
+
+ NSsync 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 NSsync. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "nssync.h"
+#include <pthread.h>
+
+static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
+static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
+static struct timespec wakets;
+static int wakets_set;
+static int wake_up;
+static struct json_value *last_result;
+static pthread_mutex_t result_mtx = PTHREAD_MUTEX_INITIALIZER;
+static pthread_cond_t result_cond = PTHREAD_COND_INITIALIZER;
+
+static void
+getts(struct timespec *ts)
+{
+ if (wakets_set) {
+ *ts = wakets;
+ wakets_set = 0;
+ } else {
+ struct timespec now;
+ clock_gettime(CLOCK_REALTIME, &now);
+
+ ts->tv_sec = now.tv_sec - now.tv_sec % periodic_timeout
+ + periodic_timeout;
+ ts->tv_nsec = 0;
+ }
+}
+
+static void
+wait_result(void)
+{
+ pthread_mutex_lock(&result_mtx);
+ while (wake_up)
+ pthread_cond_wait(&result_cond, &result_mtx);
+ pthread_mutex_unlock(&result_mtx);
+}
+
+static void
+update_result(void)
+{
+ pthread_mutex_lock(&result_mtx);
+ pthread_cond_signal(&result_cond);
+ pthread_mutex_unlock(&result_mtx);
+}
+
+void
+nssync_reschedule(int now)
+{
+ pthread_mutex_lock(&mtx);
+ if (now) {
+ wake_up = 1;
+ } else {
+ clock_gettime(CLOCK_REALTIME, &wakets);
+ wakets.tv_sec += delay_timeout;
+ wakets_set = 1;
+ }
+ pthread_cond_signal(&cond);
+ pthread_mutex_unlock(&mtx);
+ if (now)
+ wait_result();
+}
+
+void *
+nssync_timer(void *arg)
+{
+ pthread_mutex_lock(&mtx);
+ while (1) {
+ struct timespec ts;
+ getts(&ts);
+ if (debug_level >= 1) {
+ char buf[26];
+ ctime_r(&ts.tv_sec, buf);
+ buf[24] = 0;
+ debug_printf("sleeping till %s", buf);
+ }
+ if (pthread_cond_timedwait(&cond, &mtx, &ts)) {
+ if (errno && errno != ETIMEDOUT) {
+ error("pthread_cond_timedwait: %s",
+ strerror(errno));
+ abort();
+ } else
+ wake_up = 1;
+ }
+ if (wake_up) {
+ debug(1,("wake up"));
+ json_value_free(last_result);
+ last_result = NULL;
+ nssync(&last_result);
+ wake_up = 0;
+ update_result();
+ }
+ }
+ pthread_mutex_unlock(&mtx);
+ return NULL;
+}
+
+static void
+json_writer(void *closure, char const *text, size_t len)
+{
+ grecs_txtacc_t acc = closure;
+ grecs_txtacc_grow(acc, text, len);
+}
+
+char *
+nssync_format_result(void)
+{
+ struct json_format fmt;
+ grecs_txtacc_t acc;
+ char *buf;
+
+ pthread_mutex_lock(&mtx);
+
+ acc = grecs_txtacc_create();
+ fmt.indent = 0;
+ fmt.precision = 0;
+ fmt.write = json_writer;
+ fmt.data = acc;
+ json_format_value(last_result, &fmt);
+
+ grecs_txtacc_grow_char(acc, 0);
+ buf = grecs_txtacc_finish(acc, 1);
+ grecs_txtacc_free(acc);
+
+ pthread_mutex_unlock(&mtx);
+
+ return buf;
+}

Return to:

Send suggestions and report system problems to the System administrator.