Commit 8de33ea6 authored by Marek Vavrusa's avatar Marek Vavrusa

Reworked NOTIFY event mechanism to be truly thread-safe, bugfixes, more helpful output to log.

Commit refs #1253.
parent ed7295aa
......@@ -92,15 +92,15 @@ void knot_zone_contents_dump(knot_zone_contents_t *zone, char loaded_zone);
//#define KNOT_RESPONSE_DEBUG
//#define KNOT_ZONEDB_DEBUG
//#define KNOT_DNAME_DEBUG
#define KNOT_NODE_DEBUG
//#define KNOT_NODE_DEBUG
//#define KNOT_PACKET_DEBUG
//#define KNOT_EDNS_DEBUG
#define KNOT_RRSET_DEBUG
//#define KNOT_RRSET_DEBUG
//#define KNOT_NSEC3_DEBUG
//#define CUCKOO_DEBUG
//#define CUCKOO_DEBUG_HASH
#define KNOT_NS_DEBUG
#define KNOT_XFR_DEBUG
//#define KNOT_NS_DEBUG
//#define KNOT_XFR_DEBUG
//#define KNOT_DDNS_DEBUG
#ifdef KNOT_NS_DEBUG
......
......@@ -192,6 +192,11 @@ int main(int argc, char **argv)
"PID = %ld\n", (long)getpid());
}
log_server_info("PID stored in %s\n", pidfile);
size_t zcount = server->nameserver->zone_db->zone_count;
if (!zcount) {
log_server_warning("Server started, but no zones served.\n");
}
// Setup signal handler
struct sigaction sa;
......
......@@ -21,7 +21,7 @@
//#define KNOTD_JOURNAL_DEBUG
//#define KNOTD_NET_DEBUG
//#define KNOTD_ZONES_DEBUG
#define KNOTD_XFR_DEBUG
//#define KNOTD_XFR_DEBUG
//#define KNOTD_NOTIFY_DEBUG
//#define KNOTD_ZDUMP_DEBUG
//#define KNOTD_ZLOAD_DEBUG
......
......@@ -204,7 +204,7 @@ static int notify_check_and_schedule(knot_nameserver_t *nameserver,
/*----------------------------------------------------------------------------*/
int notify_process_request(knot_nameserver_t *nameserver,
int notify_process_request(knot_nameserver_t *ns,
knot_packet_t *notify,
sockaddr_t *from,
uint8_t *buffer, size_t *size)
......@@ -213,7 +213,7 @@ int notify_process_request(knot_nameserver_t *nameserver,
* - it will be fine to merge the code somehow.
*/
if (notify == NULL || nameserver == NULL || buffer == NULL
if (notify == NULL || ns == NULL || buffer == NULL
|| size == NULL || from == NULL) {
debug_notify("notify: invalid parameters for query\n");
return KNOTD_EINVAL;
......@@ -226,7 +226,10 @@ int notify_process_request(knot_nameserver_t *nameserver,
ret = knot_packet_parse_rest(notify);
if (ret != KNOT_EOK) {
debug_notify("notify: failed to parse NOTIFY query\n");
return KNOTD_EMALF;
knot_ns_error_response(ns, knot_packet_id(notify),
KNOT_RCODE_FORMERR, buffer,
size);
return KNOTD_EOK;
}
}
......@@ -235,20 +238,26 @@ int notify_process_request(knot_nameserver_t *nameserver,
ret = notify_create_response(notify, buffer, size);
if (ret != KNOTD_EOK) {
debug_notify("notify: failed to create NOTIFY response\n");
return KNOTD_ERROR; /*! \todo Some other error. */
knot_ns_error_response(ns, knot_packet_id(notify),
KNOT_RCODE_SERVFAIL, buffer,
size);
return KNOTD_EOK;
}
// find the zone
debug_notify("notify: looking up zone by name\n");
const knot_dname_t *qname = knot_packet_qname(notify);
const knot_zone_t *z = knot_zonedb_find_zone_for_name(
nameserver->zone_db, qname);
ns->zone_db, qname);
if (z == NULL) {
debug_notify("notify: failed to find zone by name\n");
return KNOTD_ERROR; /*! \todo Some other error. */
knot_ns_error_response(ns, knot_packet_id(notify),
KNOT_RCODE_REFUSED, buffer,
size);
return KNOTD_EOK;
}
notify_check_and_schedule(nameserver, z, from);
notify_check_and_schedule(ns, z, from);
return KNOTD_EOK;
}
......@@ -281,9 +290,9 @@ int notify_process_response(knot_nameserver_t *nameserver,
/* Match ID against awaited. */
zonedata_t *zd = (zonedata_t *)knot_zone_data(zone);
pthread_mutex_lock(&zd->lock);
uint16_t pkt_id = knot_packet_id(notify);
notify_ev_t *ev = 0, *match = 0;
pthread_mutex_lock(&zd->lock);
WALK_LIST(ev, zd->notify_pending) {
if ((int)pkt_id == ev->msgid) {
match = ev;
......@@ -299,14 +308,9 @@ int notify_process_response(knot_nameserver_t *nameserver,
}
/* NOTIFY is now finished. */
evsched_t *sched = ((server_t *)knot_ns_get_data(nameserver))->sched;
if (match->timer) {
log_zone_info("NOTIFY query for zone %s answered.\n",
zd->conf->name);
evsched_cancel(sched, (event_t *)match->timer);
evsched_schedule(sched, (event_t *)match->timer, 0);
match->timer = 0;
}
zones_cancel_notify(zd, match);
/* Zone was removed/reloaded. */
pthread_mutex_unlock(&zd->lock);
log_server_info("Received response for pending NOTIFY query ID=%u\n",
......
......@@ -32,7 +32,7 @@ typedef struct notify_ev_t {
int retries; /*!< Number of retries. */
int msgid; /*!< ID of pending NOTIFY. */
sockaddr_t addr; /*!< Slave server address. */
volatile struct event_t *timer; /*!< Event timer. */
struct event_t *timer; /*!< Event timer. */
knot_zone_t *zone; /*!< Associated zone. */
} notify_ev_t;
......
......@@ -42,6 +42,7 @@ static int evsched_run(dthread_t *thread)
/* Process termination event. */
if (ev->type == EVSCHED_TERM) {
evsched_event_finished(s);
evsched_event_free(s, ev);
break;
}
......@@ -49,7 +50,9 @@ static int evsched_run(dthread_t *thread)
/* Process event. */
if (ev->type == EVSCHED_CB && ev->cb) {
ev->cb(ev);
evsched_event_finished(s);
} else {
evsched_event_finished(s);
evsched_event_free(s, ev);
}
......
......@@ -64,12 +64,9 @@ int udp_handle(uint8_t *qbuf, size_t qbuflen, size_t *resp_len,
case KNOT_RESPONSE_NORMAL:
res = zones_process_response(ns, addr, packet,
qbuf, resp_len);
// res = knot_ns_process_response();
break;
case KNOT_RESPONSE_AXFR:
case KNOT_RESPONSE_IXFR:
/*! \todo SLAVE DISABLED */
break;
case KNOT_RESPONSE_NOTIFY:
res = notify_process_response(ns, packet, addr,
qbuf, resp_len);
......@@ -91,8 +88,6 @@ int udp_handle(uint8_t *qbuf, size_t qbuflen, size_t *resp_len,
case KNOT_QUERY_NOTIFY:
res = notify_process_request(ns, packet, addr,
qbuf, resp_len);
*resp_len = 0;
res = KNOTD_EOK;
break;
case KNOT_QUERY_UPDATE:
default:
......
......@@ -96,7 +96,6 @@ static inline void qr_response_ev(struct ev_loop *loop, ev_io *w, int revents)
msg.msg_namelen = qw->addr.len;
/* Receive msg. */
debug_xfr("qr_response_ev: reading response\n");
ssize_t n = recvmsg(w->fd, &msg, 0);
size_t resp_len = sizeof(qbuf);
if (n > 0) {
......@@ -109,8 +108,10 @@ static inline void qr_response_ev(struct ev_loop *loop, ev_io *w, int revents)
((server_t *)knot_ns_get_data(qw->ns))->sched;
if (qw->ev) {
evsched_cancel(sched, qw->ev);
evsched_event_free(sched, qw->ev);
qw->ev = 0;
if (qw->ev) {
evsched_event_free(sched, qw->ev);
qw->ev = 0;
}
}
/* Close after receiving response. */
......@@ -204,7 +205,7 @@ static inline void xfr_client_ev(struct ev_loop *loop, ev_io *w, int revents)
knotd_strerror(ret));
}
}
debug_xfr("xfr_client_ev: AXFR/IN transfer finished\n");
log_server_info("AXFR/IN transfer finished.\n");
break;
case XFR_TYPE_IIN:
/* Save changesets. */
......@@ -228,7 +229,7 @@ static inline void xfr_client_ev(struct ev_loop *loop, ev_io *w, int revents)
free(chs->sets);
free(chs);
request->data = 0;
debug_xfr("xfr_client_ev: IXFR/IN transfer finished\n");
log_server_info("IXFR/IN transfer finished.\n");
break;
default:
ret = KNOTD_EINVAL;
......@@ -314,14 +315,49 @@ static inline void xfr_bridge_ev(struct ev_loop *loop, ev_io *w, int revents)
return;
}
/* Update address. */
sockaddr_update(&req->addr);
int r_port = -1;
#ifdef DISABLE_IPV6
char r_addr[INET_ADDRSTRLEN];
memset(r_addr, 0, sizeof(r_addr));
#else
/* Load IPv6 addr if default. */
char r_addr[INET6_ADDRSTRLEN];
memset(r_addr, 0, sizeof(r_addr));
if (req->addr.family == AF_INET6) {
r_port = ntohs(req->addr.addr6.sin6_port);
inet_ntop(req->addr.family, &req->addr.addr6.sin6_addr,
r_addr, sizeof(r_addr));
}
#endif
/* Load IPv4 if set. */
if (req->addr.family == AF_INET) {
r_port = ntohs(req->addr.addr4.sin_port);
inet_ntop(req->addr.family, &req->addr.addr4.sin_addr,
r_addr, sizeof(r_addr));
}
/* Connect to remote. */
if (req->session <= 0) {
int fd = socket_create(req->addr.family, SOCK_STREAM);
if (fd < 0) {
log_server_warning("Failed to create socket "
"(type=%s, family=%s).\n",
"SOCK_STREAM",
req->addr.family == AF_INET ?
"AF_INET" : "AF_INET6");
return;
}
ret = connect(fd, req->addr.ptr, req->addr.len);
if (ret < 0) {
log_server_warning("Failed to connect to %cXFR master "
"at %s:%d.\n",
req->type == XFR_TYPE_AIN ? 'A' : 'I',
r_addr, r_port);
if (!knot_zone_contents(zone)) {
log_zone_notice("Zone AXFR bootstrap failed.\n");
}
return;
}
......@@ -337,7 +373,8 @@ static inline void xfr_bridge_ev(struct ev_loop *loop, ev_io *w, int revents)
const knot_zone_contents_t *contents = knot_zone_contents(zone);
if (!contents && req->type == XFR_TYPE_IIN) {
rcu_read_unlock();
debug_xfr("xfr_in: failed start IXFR on zone with no contents\n");
log_server_warning("Failed start IXFR on zone with no "
"contents\n");
socket_close(req->session);
return;
}
......@@ -369,11 +406,13 @@ static inline void xfr_bridge_ev(struct ev_loop *loop, ev_io *w, int revents)
}
/* Send XFR query. */
debug_xfr("xfr_in: sending XFR query (%zu bytes)\n", bufsize);
log_server_info("Sending %cXFR query to %s:%d (%zu bytes).\n",
req->type == XFR_TYPE_AIN ? 'A' : 'I',
r_addr, r_port, bufsize);
ret = req->send(req->session, &req->addr, req->wire, bufsize);
if (ret != bufsize) {
debug_xfr("xfr_in: failed to send XFR query type %d\n",
req->type);
log_server_notice("Failed to send %cXFR query.",
req->type == XFR_TYPE_AIN ? 'A' : 'I');
socket_close(req->session);
return;
}
......@@ -569,14 +608,14 @@ int xfr_master(dthread_t *thread)
char r_addr[INET6_ADDRSTRLEN];
memset(r_addr, 0, sizeof(r_addr));
if (xfr.addr.family == AF_INET6) {
r_port = xfr.addr.addr6.sin6_port;
r_port = ntohs(xfr.addr.addr6.sin6_port);
inet_ntop(xfr.addr.family, &xfr.addr.addr6.sin6_addr,
r_addr, sizeof(r_addr));
}
#endif
/* Load IPv4 if set. */
if (xfr.addr.family == AF_INET) {
r_port = xfr.addr.addr4.sin_port;
r_port = ntohs(xfr.addr.addr4.sin_port);
inet_ntop(xfr.addr.family, &xfr.addr.addr4.sin_addr,
r_addr, sizeof(r_addr));
}
......
......@@ -64,18 +64,9 @@ static int zonedata_destroy(knot_zone_t *zone)
/* Remove list of pending NOTIFYs. */
pthread_mutex_lock(&zd->lock);
node *n = 0;
WALK_LIST(n, zd->notify_pending) {
notify_ev_t *ev = (notify_ev_t *)n;
if (ev->timer) {
ev->zone = 0;
evsched_t *sch = ev->timer->parent;
evsched_cancel(sch, (event_t *)ev->timer);
evsched_event_free(sch, (event_t *)ev->timer);
rem_node(&ev->n);
ev->timer = 0;
free(ev);
}
notify_ev_t *ev = 0, *evn = 0;
WALK_LIST_DELSAFE(ev, evn, zd->notify_pending) {
zones_cancel_notify(zd, ev);
}
pthread_mutex_unlock(&zd->lock);
......@@ -255,8 +246,10 @@ static int zones_expire_ev(event_t *e)
zonedata_t *zd = (zonedata_t *)zone->data;
if (zd->xfr_in.timer) {
evsched_cancel(e->parent, zd->xfr_in.timer);
evsched_event_free(e->parent, zd->xfr_in.timer);
zd->xfr_in.timer = 0;
if (zd->xfr_in.timer) {
evsched_event_free(e->parent, zd->xfr_in.timer);
zd->xfr_in.timer = 0;
}
}
/* Delete self. */
......@@ -311,9 +304,6 @@ static int zones_refresh_ev(event_t *e)
if (!knot_zone_contents(zone)) {
/* Bootstrap from XFR master. */
evsched_cancel(e->parent, e);
/* Prepare XFR client transfer. */
knot_ns_xfr_t xfr_req;
memset(&xfr_req, 0, sizeof(knot_ns_xfr_t));
memcpy(&xfr_req.addr, &zd->xfr_in.master, sizeof(sockaddr_t));
......@@ -327,7 +317,8 @@ static int zones_refresh_ev(event_t *e)
rcu_read_unlock();
/* Enqueue XFR request. */
debug_zones("xfr_in: attempting to bootstrap zone from master\n");
log_zone_info("Attempting to bootstrap zone %s from master\n",
zd->conf->name);
return xfr_request(zd->server->xfr_h, &xfr_req);
}
......@@ -393,30 +384,36 @@ static int zones_notify_send(event_t *e)
{
notify_ev_t *ev = (notify_ev_t *)e->data;
knot_zone_t *zone = ev->zone;
knot_zone_contents_t *contents = knot_zone_get_contents(zone);
if (!zone || !knot_zone_data(zone) || !contents) {
if (!zone) {
log_zone_error("notify: NOTIFY invalid event received\n");
evsched_event_free(e->parent, (event_t *)ev->timer);
evsched_event_free(e->parent, e);
free(ev);
return KNOTD_EINVAL;
}
/* Check for answered/cancelled query. */
zonedata_t *zd = (zonedata_t *)knot_zone_data(zone);
pthread_mutex_lock(&zd->lock);
if (ev->timer == NULL) {
debug_zones("notify: cancelling old NOTIFY timer for %s.\n",
zd->conf->name);
evsched_cancel(e->parent, e);
evsched_event_free(e->parent, e);
knot_zone_contents_t *contents = knot_zone_get_contents(zone);
debug_notify("notify: NOTIFY timer event\n");
/* Reduce number of available retries. */
--ev->retries;
/* Check number of retries. */
if (ev->retries < 0) {
log_server_notice("NOTIFY query maximum number of retries "
"for zone %s exceeded.\n",
zd->conf->name);
pthread_mutex_lock(&zd->lock);
debug_notify("notify: Deleting NOTIFY event because "
"maximum number of retries was reached.\n");
rem_node(&ev->n);
pthread_mutex_unlock(&zd->lock);
evsched_event_free(e->parent, e);
free(ev);
return KNOTD_EOK;
pthread_mutex_unlock(&zd->lock);
return KNOTD_EMALF;
}
pthread_mutex_unlock(&zd->lock);
debug_zones("notify: NOTIFY timer event\n");
/* Prepare buffer for query. */
uint8_t qbuf[SOCKET_MTU_SZ];
......@@ -456,32 +453,14 @@ static int zones_notify_send(event_t *e)
}
/* Reduce number of available retries. */
--ev->retries;
/* Check number of retries. */
if (ev->retries <= 0) {
log_server_notice("NOTIFY query maximum number of retries "
"for zone %s exceeded.\n",
zd->conf->name);
pthread_mutex_lock(&zd->lock);
if (ev->timer) {
evsched_cancel(e->parent, e);
evsched_schedule(e->parent, e, 0);
ev->timer = 0;
}
pthread_mutex_unlock(&zd->lock);
return KNOTD_EMALF;
}
/* RFC suggests 60s, but it is configurable. */
int retry_tmr = ev->timeout * 1000;
/* Reschedule. */
evsched_schedule(e->parent, e, retry_tmr);
debug_zones("notify: RETRY after %u secs\n",
retry_tmr / 1000);
debug_notify("notify: RETRY after %u secs\n",
retry_tmr / 1000);
return ret;
}
......@@ -1039,8 +1018,7 @@ static int zones_insert_zones(knot_nameserver_t *ns,
int ret = KNOT_ERROR;
if (reload) {
/* Zone file not exists and has master set. */
/*! \todo SLAVE DISABLED Bootstrapping is disabled. */
if (0 && stat_ret < 0 && !EMPTY_LIST(z->acl.xfr_in)) {
if (stat_ret < 0 && !EMPTY_LIST(z->acl.xfr_in)) {
/* Create stub database. */
debug_zones("Loading stub zone for bootstrap.\n");
......@@ -1998,27 +1976,39 @@ int zones_timers_update(knot_zone_t *zone, conf_zone_t *cfzone, evsched_t *sch)
/* Remove list of pending NOTIFYs. */
pthread_mutex_lock(&zd->lock);
node *n = 0;
WALK_LIST(n, zd->notify_pending) {
notify_ev_t *ev = (notify_ev_t *)n;
if (ev->timer) {
evsched_cancel(sch, (event_t *)ev->timer);
evsched_schedule(sch, (event_t *)ev->timer, 0);
ev->timer = 0;
}
notify_ev_t *ev = 0, *evn = 0;
WALK_LIST_DELSAFE(ev, evn, zd->notify_pending) {
zones_cancel_notify(zd, ev);
}
pthread_mutex_unlock(&zd->lock);
/* Check XFR/IN master server. */
/*! \todo SLAVE DISABLED */
// if (zd->xfr_in.master.ptr) {
if (zd->xfr_in.master.ptr) {
// /* Schedule REFRESH timer. */
// uint32_t refresh_tmr = zones_soa_refresh(zone);
// zd->xfr_in.timer = evsched_schedule_cb(sch, zones_refresh_ev,
// zone, refresh_tmr);
// debug_zones("notify: REFRESH set to %u\n", refresh_tmr);
// }
/* Schedule REFRESH timer. */
uint32_t refresh_tmr = zones_soa_refresh(zone);
zd->xfr_in.timer = evsched_schedule_cb(sch, zones_refresh_ev,
zone, refresh_tmr);
debug_zones("notify: REFRESH set to %u\n", refresh_tmr);
}
/* Schedule IXFR database syncing. */
/*! \todo Sync timer should not be reset after each xfr. */
int sync_timeout = cfzone->dbsync_timeout * 1000; /* Convert to ms. */
if (zd->ixfr_dbsync) {
evsched_cancel(sch, zd->ixfr_dbsync);
evsched_event_free(sch, zd->ixfr_dbsync);
zd->ixfr_dbsync = 0;
}
zd->ixfr_dbsync = evsched_schedule_cb(sch,
zones_zonefile_sync_ev,
zone, sync_timeout);
/* Do not issue NOTIFY queries if stub. */
if (!knot_zone_contents(zone)) {
conf_read_unlock();
return KNOTD_EOK;
}
/* Schedule NOTIFY to slaves. */
conf_remote_t *r = 0;
......@@ -2050,7 +2040,7 @@ int zones_timers_update(knot_zone_t *zone, conf_zone_t *cfzone, evsched_t *sch)
/* Prepare request. */
ev->retries = cfzone->notify_retries + 1; /* first + N retries*/
ev->msgid = -1;
ev->msgid = 0;
ev->zone = zone;
ev->timeout = cfzone->notify_timeout;
......@@ -2066,19 +2056,49 @@ int zones_timers_update(knot_zone_t *zone, conf_zone_t *cfzone, evsched_t *sch)
tmr_s, cfg_if->address, cfg_if->port);
}
/* Schedule IXFR database syncing. */
/*! \todo Sync timer should not be reset after each xfr. */
int sync_timeout = cfzone->dbsync_timeout * 1000; /* Convert to ms. */
if (zd->ixfr_dbsync) {
evsched_cancel(sch, zd->ixfr_dbsync);
evsched_event_free(sch, zd->ixfr_dbsync);
zd->ixfr_dbsync = 0;
conf_read_unlock();
return KNOTD_EOK;
}
int zones_cancel_notify(zonedata_t *zd, notify_ev_t *ev)
{
if (!zd || !ev || !ev->timer) {
return KNOTD_EINVAL;
}
zd->ixfr_dbsync = evsched_schedule_cb(sch,
zones_zonefile_sync_ev,
zone, sync_timeout);
conf_read_unlock();
/* Wait for event to finish running. */
int pkt_id = ev->msgid; /*< Do not optimize! */
event_t *tmr = ev->timer;
ev->timer = 0;
pthread_mutex_unlock(&zd->lock);
evsched_cancel(tmr->parent, tmr);
/* Re-lock and find again (if not deleted). */
pthread_mutex_lock(&zd->lock);
int match_exists = 0;
notify_ev_t *tmpev = 0;
WALK_LIST(tmpev, zd->notify_pending) {
if (tmpev == ev) {
match_exists = 1;
break;
}
}
/* Event deleted before cancelled. */
if (!match_exists) {
debug_notify("notify: NOTIFY event for query ID=%u was"
" deleted before cancellation.\n",
pkt_id);
return KNOTD_EOK;
}
/* Free event (won't be scheduled again). */
debug_notify("notify: NOTIFY query ID=%u event cancelled.\n",
pkt_id);
rem_node(&ev->n);
evsched_event_free(tmr->parent, tmr);
free(ev);
return KNOTD_EOK;
}
......@@ -18,6 +18,7 @@
#include "libknot/nameserver/name-server.h"
#include "libknot/zone/zonedb.h"
#include "knot/conf/conf.h"
#include "knot/server/notify.h"
#include "knot/server/server.h"
#include "knot/server/journal.h"
#include "libknot/zone/zone.h"
......@@ -231,6 +232,20 @@ int zones_apply_changesets(knot_ns_xfr_t *xfr);
*/
int zones_timers_update(knot_zone_t *zone, conf_zone_t *cfzone, evsched_t *sch);
/*!
* \brief Cancel pending NOTIFY timer.
*
* \warning Expects locked zonedata lock.
*
* \param zd Zone data.
* \param ev NOTIFY event.
*
* \retval KNOTD_EOK
* \retval KNOTD_ERROR
* \retval KNOTD_EINVAL
*/
int zones_cancel_notify(zonedata_t *zd, notify_ev_t *ev);
#endif // _KNOTD_ZONES_H_
/*! @} */
......@@ -116,6 +116,7 @@ static int events_tests_run(int argc, char *argv[])
// 3. Wait for next event
e = evsched_next(s);
evsched_event_finished(s);
gettimeofday(&rt, 0);
ok(e != 0, "evsched: received valid event");
......@@ -135,7 +136,7 @@ static int events_tests_run(int argc, char *argv[])
lives_ok({evsched_event_free(s, e);}, "evsched: deleted event");
// 7. Insert and immediately cancel an event
e = evsched_schedule_cb(s, 0, (void*)0xdead, 10);
e = evsched_schedule_cb(s, 0, (void*)0xdead, 1000);
ret = evsched_cancel(s, e);
ok(ret == 0, "evsched: inserted and cancelled an event");
if (e) {
......@@ -146,6 +147,7 @@ static int events_tests_run(int argc, char *argv[])
pthread_t t;
pthread_create(&t, 0, term_thr, s);
e = evsched_next(s);
evsched_event_finished(s);
ok(e != 0, "evsched: received termination event");
// 9. Termination event is valid
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment