From c79baf1b1faf5477773efdd53513f58cb361a5cd Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 14 Apr 2019 14:36:41 +0200 Subject: [PATCH 01/13] * Optimized validation of large lists * New xmldb_get1() returning actual cache - not a copy. This has lead to some householding instead of just deleting the copy * xml_diff rewritten to work linearly instead of O(2) * New xml_insert function using tree search. The new code uses this in insertion xmldb_put and defaults. (Note previous xml_insert renamed to xml_wrap_all) --- CHANGELOG.md | 5 +- include/clixon_custom.h | 4 +- lib/clixon/clixon_xml.h | 4 +- lib/clixon/clixon_xml_sort.h | 6 +- lib/src/clixon_datastore_write.c | 50 +++++-- lib/src/clixon_json.c | 75 ---------- lib/src/clixon_xml.c | 42 +++++- lib/src/clixon_xml_map.c | 126 ++++++++++------- lib/src/clixon_xml_sort.c | 229 ++++++++++++++++++++----------- lib/src/clixon_yang.c | 46 ++++++- test/README.md | 4 + test/plot_perf.sh | 184 +++++++++++++++++-------- test/test_insert.sh | 215 +++++++++++++++++++++++++++++ test/test_perf.sh | 29 ++-- util/Makefile.in | 4 + util/clixon_util_insert.c | 215 +++++++++++++++++++++++++++++ 16 files changed, 937 insertions(+), 301 deletions(-) create mode 100755 test/test_insert.sh create mode 100644 util/clixon_util_insert.c diff --git a/CHANGELOG.md b/CHANGELOG.md index bde6ab36..20564c15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,7 +92,10 @@ ``` ### Minor changes -* Optimized validation by making xml_diff work on raw cache tree (not copies) +* Optimized validation of large lists + * New xmldb_get1() returning actual cache - not a copy. This has lead to some householding instead of just deleting the copy + * xml_diff rewritten to work linearly instead of O(2) + * New xml_insert function using tree search. The new code uses this in insertion xmldb_put and defaults. (Note previous xml_insert renamed to xml_wrap_all) * Added syntactic check for yang status: current, deprecated or obsolete. * Added `xml_wrap` function that adds an XML node above a node as a wrapper * also renamed `xml_insert` to `xml_wrap_all`. diff --git a/include/clixon_custom.h b/include/clixon_custom.h index d06a9e50..c302d0ed 100644 --- a/include/clixon_custom.h +++ b/include/clixon_custom.h @@ -41,4 +41,6 @@ */ #undef RPC_USERNAME_ASSERT - +/* Use new xml_insert code on sorted xml lists + */ +#define USE_XML_INSERT diff --git a/lib/clixon/clixon_xml.h b/lib/clixon/clixon_xml.h index ef6395a7..879aa453 100644 --- a/lib/clixon/clixon_xml.h +++ b/lib/clixon/clixon_xml.h @@ -118,8 +118,9 @@ cxobj *xml_child_i_type(cxobj *xn, int i, enum cxobj_type type); cxobj *xml_child_i_set(cxobj *xt, int i, cxobj *xc); cxobj *xml_child_each(cxobj *xparent, cxobj *xprev, enum cxobj_type type); -cxobj **xml_childvec_get(cxobj *x); +int xml_child_insert_pos(cxobj *x, cxobj *xc, int i); int xml_childvec_set(cxobj *x, int len); +cxobj **xml_childvec_get(cxobj *x); cxobj *xml_new(char *name, cxobj *xn_parent, yang_stmt *spec); yang_stmt *xml_spec(cxobj *x); int xml_spec_set(cxobj *x, yang_stmt *spec); @@ -130,7 +131,6 @@ cxobj *xml_find(cxobj *xn_parent, char *name); int xml_addsub(cxobj *xp, cxobj *xc); cxobj *xml_wrap_all(cxobj *xp, char *tag); cxobj *xml_wrap(cxobj *xc, char *tag); -#define xml_insert(x,t) xml_wrap_all((x),(t)) int xml_purge(cxobj *xc); int xml_child_rm(cxobj *xp, int i); int xml_rm(cxobj *xc); diff --git a/lib/clixon/clixon_xml_sort.h b/lib/clixon/clixon_xml_sort.h index 558d3c11..a75c1358 100644 --- a/lib/clixon/clixon_xml_sort.h +++ b/lib/clixon/clixon_xml_sort.h @@ -40,8 +40,10 @@ * Prototypes */ int xml_child_spec(cxobj *x, cxobj *xp, yang_stmt *yspec, yang_stmt **yp); +int xml_cmp(cxobj *x1, cxobj *x2, int enm); int xml_sort(cxobj *x0, void *arg); -int xml_sort_verify(cxobj *x, void *arg); -int match_base_child(cxobj *x0, cxobj *x1c, yang_stmt *yc, cxobj **x0cp); +int xml_insert(cxobj *xp, cxobj *xc); +int xml_sort_verify(cxobj *x, void *arg); +int match_base_child(cxobj *x0, cxobj *x1c, yang_stmt *yc, cxobj **x0cp); #endif /* _CLIXON_XML_SORT_H */ diff --git a/lib/src/clixon_datastore_write.c b/lib/src/clixon_datastore_write.c index b1727945..9e7e260b 100644 --- a/lib/src/clixon_datastore_write.c +++ b/lib/src/clixon_datastore_write.c @@ -128,9 +128,7 @@ text_modify(clicon_handle h, int i; int ret; int changed = 0; /* Only if x0p's children have changed-> sort is necessary */ - - assert(x1 && xml_type(x1) == CX_ELMNT); - assert(y0); + /* Check for operations embedded in tree according to netconf */ if ((opstr = xml_find_value(x1, "operation")) != NULL) if (xml_operation(opstr, &op) < 0) @@ -157,8 +155,16 @@ text_modify(clicon_handle h, permit = 1; } // int iamkey=0; - if ((x0 = xml_new(x1name, x0p, (yang_stmt*)y0)) == NULL) + +#ifdef USE_XML_INSERT + /* Add new xml node but without parent - insert when node fully + copied (see changed conditional below) */ + if ((x0 = xml_new(x1name, NULL, (yang_stmt*)y0)) == NULL) goto done; +#else + if ((x0 = xml_new(x1name, x0p, (yang_stmt*)y0)) == NULL) + goto done; +#endif changed++; /* Copy xmlns attributes */ @@ -204,6 +210,12 @@ text_modify(clicon_handle h, } } } +#ifdef USE_XML_INSERT + if (changed){ + if (xml_insert(x0p, x0) < 0) + goto done; + } +#endif break; case OP_DELETE: if (x0==NULL){ @@ -282,8 +294,15 @@ text_modify(clicon_handle h, goto fail; permit = 1; } +#ifdef USE_XML_INSERT + /* Add new xml node but without parent - insert when node fully + copied (see changed conditional below) */ + if ((x0 = xml_new(x1name, NULL, (yang_stmt*)y0)) == NULL) + goto done; +#else if ((x0 = xml_new(x1name, x0p, (yang_stmt*)y0)) == NULL) goto done; +#endif changed++; /* Copy xmlns attributes */ x1a = NULL; @@ -345,6 +364,12 @@ text_modify(clicon_handle h, if (ret == 0) goto fail; } +#ifdef USE_XML_INSERT + if (changed){ + if (xml_insert(x0p, x0) < 0) + goto done; + } +#endif break; case OP_DELETE: if (x0==NULL){ @@ -362,15 +387,16 @@ text_modify(clicon_handle h, } if (xml_purge(x0) < 0) goto done; - changed++; } break; default: break; } /* CONTAINER switch op */ } /* else Y_CONTAINER */ +#ifndef USE_XML_INSERT if (changed) xml_sort(x0p, NULL); +#endif retval = 1; done: if (x0vec) @@ -417,8 +443,8 @@ text_modify_top(clicon_handle h, int ret; /* Assure top-levels are 'config' */ - assert(x0 && strcmp(xml_name(x0),"config")==0); - assert(x1 && strcmp(xml_name(x1),"config")==0); + // assert(x0 && strcmp(xml_name(x0),"config")==0); + // assert(x1 && strcmp(xml_name(x1),"config")==0); /* Check for operations embedded in tree according to netconf */ if ((opstr = xml_find_value(x1, "operation")) != NULL) @@ -583,11 +609,11 @@ xml_container_presence(cxobj *x, */ int xmldb_put(clicon_handle h, - const char *db, - enum operation_type op, - cxobj *x1, - char *username, - cbuf *cbret) + const char *db, + enum operation_type op, + cxobj *x1, + char *username, + cbuf *cbret) { int retval = -1; char *dbfile = NULL; diff --git a/lib/src/clixon_json.c b/lib/src/clixon_json.c index 428831e4..0d06e202 100644 --- a/lib/src/clixon_json.c +++ b/lib/src/clixon_json.c @@ -915,79 +915,4 @@ json_parse_file(int fd, return retval; } -/* - * Turn this on to get a json parse and pretty print test program - * Usage: json - * read json from input - * Example compile: - gcc -g -o json -I. -I../clixon ./clixon_json.c -lclixon -lcligen - * Example run: - echo '{"foo": -23}' | ./json -*/ -#if 0 /* Test program */ - -static int -usage(char *argv0) -{ - fprintf(stderr, "usage:%s.\n\tInput on stdin\n", argv0); - exit(0); -} - -int -main(int argc, - char **argv) -{ - cxobj *xt; - cxobj *xc; - cbuf *cb = cbuf_new(); - char *buf = NULL; - int i; - int c; - int len; - FILE *f = stdin; - - if (argc != 1){ - usage(argv[0]); - return 0; - } - clicon_log_init(__FILE__, LOG_INFO, CLICON_LOG_STDERR); - len = 1024; /* any number is fine */ - if ((buf = malloc(len)) == NULL){ - perror("malloc"); - return -1; - } - memset(buf, 0, len); - - i = 0; /* position in buf */ - while (1){ /* read the whole file */ - if ((c = fgetc(f)) == EOF) - break; - if (len==i){ - if ((buf = realloc(buf, 2*len)) == NULL){ - fprintf(stderr, "%s: realloc: %s\n", __FUNCTION__, strerror(errno)); - goto done; - } - memset(buf+len, 0, len); - len *= 2; - } - buf[i++] = (char)(c&0xff); - } /* read a line */ - - if (json_parse_str(buf, &xt) < 0) - return -1; - xc = NULL; - while ((xc = xml_child_each(xt, xc, -1)) != NULL) { - xmltree2cbuf(cb, xc, 0); /* dump data structures */ - //clicon_xml2cbuf(cb, xc, 0, 1); /* print xml */ - } - fprintf(stdout, "%s", cbuf_get(cb)); - if (xt) - xml_free(xt); - if (cb) - cbuf_free(cb); - done: - return 0; -} - -#endif /* Test program */ diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index 957a8135..19a2c5f7 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -562,6 +562,7 @@ xml_child_nr_type(cxobj *xn, * @param[in] i the number of the child, eg order in children vector * @retval xml The child xml node * @retval NULL if no such child, or empty child + * @see xml_child_i_type */ cxobj * xml_child_i(cxobj *xn, @@ -652,7 +653,7 @@ xml_child_each(cxobj *xparent, } /*! Extend child vector with one and insert xml node there - * Note: does not do anything with child, you may need to set its parent, etc + * @note does not do anything with child, you may need to set its parent, etc */ static int xml_child_append(cxobj *x, @@ -668,7 +669,31 @@ xml_child_append(cxobj *x, return 0; } -/*! Set a a childvec to a specific size, fill with children after +/*! Insert child xc at position i under parent xp + * + * @see xml_child_append + * @note does not do anything with child, you may need to set its parent, etc + */ +int +xml_child_insert_pos(cxobj *xp, + cxobj *xc, + int i) +{ + size_t size; + + xp->x_childvec_len++; + xp->x_childvec = realloc(xp->x_childvec, xp->x_childvec_len*sizeof(cxobj*)); + if (xp->x_childvec == NULL){ + clicon_err(OE_XML, errno, "realloc"); + return -1; + } + size = (xml_child_nr(xp) - i - 1)*sizeof(cxobj *); + memmove(&xp->x_childvec[i+1], &xp->x_childvec[i], size); + xp->x_childvec[i] = xc; + return 0; +} + +/*! Set a childvec to a specific size, fill with children after * @code * xml_childvec_set(x, 2); * xml_child_i_set(x, 0, xc0) @@ -731,6 +756,7 @@ xml_new(char *name, xml_parent_set(x, xp); if (xml_child_append(xp, x) < 0) return NULL; + x->_x_i = xml_child_nr(xp)-1; } x->x_spec = yspec; /* Can be NULL */ return x; @@ -966,15 +992,14 @@ xml_child_rm(cxobj *xp, int xml_rm(cxobj *xc) { - int retval = 0; + int retval = -1; cxobj *xp; cxobj *x; int i; if ((xp = xml_parent(xc)) == NULL) - goto done; - retval = -1; - /* Find child in parent */ + goto ok; + /* Find child in parent XXX: search? */ x = NULL; i = 0; while ((x = xml_child_each(xp, x, -1)) != NULL) { if (x == xc) @@ -982,7 +1007,10 @@ xml_rm(cxobj *xc) i++; } if (x != NULL) - retval = xml_child_rm(xp, i); + if (xml_child_rm(xp, i) < 0) + goto done; + ok: + retval = 0; done: return retval; } diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index ff85a4d2..06eaf79e 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -1060,6 +1060,13 @@ cvec2xml_1(cvec *cvv, * @param[out] changed_x0 Pointervector to XML nodes changed orig value * @param[out] changed_x1 Pointervector to XML nodes changed wanted value * @param[out] changedlen Length of changed vector + * Algorithm to compare two sorted lists A, B: + * A 0 1 2 3 5 6 + * B 0 2 4 5 6 + * Let a,b be first elements of A,B respectively + * a = b : recurse; get next a,b + * a < b : add a in x0, get next a + * a > b : add b in x1, get next b */ static int xml_diff1(yang_stmt *ys, @@ -1079,35 +1086,54 @@ xml_diff1(yang_stmt *ys, yang_stmt *yc; char *b1; char *b2; + int eq; - clicon_debug(2, "%s: %s", __FUNCTION__, ys->ys_argument?ys->ys_argument:"yspec"); - /* Check nodes present in x0 and x1 + nodes only in x0 - * Loop over x0 - * XXX: room for improvement. Compare with match_base_child() - */ - x0c = NULL; - while ((x0c = xml_child_each(x0, x0c, CX_ELMNT)) != NULL){ - if ((yc = xml_spec(x0c)) == NULL){ - clicon_err(OE_UNIX, errno, "Unknown element: %s", xml_name(x0c)); - goto done; + /* Traverse x0 and x1 in lock-step */ + x0c = x1c = NULL; + x0c = xml_child_each(x0, x0c, CX_ELMNT); + x1c = xml_child_each(x1, x1c, CX_ELMNT); + for (;;){ + if (x0c == NULL && x1c == NULL) + goto ok; + else if (x0c == NULL){ + if (cxvec_append(x1c, x1vec, x1veclen) < 0) + goto done; + x1c = xml_child_each(x1, x1c, CX_ELMNT); + continue; } - /* Does x1 have a child matching x0c? */ - if (match_base_child(x1, x0c, yc, &x1c) < 0) - goto done; - if (x1c == NULL){ + else if (x1c == NULL){ if (cxvec_append(x0c, x0vec, x0veclen) < 0) goto done; + x0c = xml_child_each(x0, x0c, CX_ELMNT); + continue; } - else if (yang_choice(yc)){ - /* if x0c and x1c are choice/case, then they are changed */ - if (cxvec_append(x0c, changed_x0, changedlen) < 0) - goto done; - (*changedlen)--; /* append two vectors */ - if (cxvec_append(x1c, changed_x1, changedlen) < 0) + /* Both x0c and x1c exists, check if they are equal. */ + eq = xml_cmp(x0c, x1c, 0); + if (eq < 0){ + if (cxvec_append(x0c, x0vec, x0veclen) < 0) goto done; + x0c = xml_child_each(x0, x0c, CX_ELMNT); } - else{ /* if x0c and x1c are leafs w bodies, then they are changed */ - if (yc->ys_keyword == Y_LEAF){ + else if (eq > 0){ + if (cxvec_append(x1c, x1vec, x1veclen) < 0) + goto done; + x1c = xml_child_each(x1, x1c, CX_ELMNT); + } + else{ /* equal */ + if ((yc = xml_spec(x0c)) == NULL){ + clicon_err(OE_UNIX, errno, "Unknown element: %s", xml_name(x0c)); + goto done; + } + if (yang_choice(yc)){ + /* if x0c and x1c are choice/case, then they are changed */ + if (cxvec_append(x0c, changed_x0, changedlen) < 0) + goto done; + (*changedlen)--; /* append two vectors */ + if (cxvec_append(x1c, changed_x1, changedlen) < 0) + goto done; + } + else if (yc->ys_keyword == Y_LEAF){ + /* if x0c and x1c are leafs w bodies, then they are changed */ if ((b1 = xml_body(x0c)) == NULL) /* empty type */ break; if ((b2 = xml_body(x1c)) == NULL) /* empty type */ @@ -1120,29 +1146,16 @@ xml_diff1(yang_stmt *ys, goto done; } } - if (xml_diff1(yc, x0c, x1c, - x0vec, x0veclen, - x1vec, x1veclen, - changed_x0, changed_x1, changedlen)< 0) + else if (xml_diff1(yc, x0c, x1c, + x0vec, x0veclen, + x1vec, x1veclen, + changed_x0, changed_x1, changedlen)< 0) goto done; } - } /* while x0 */ - /* Check nodes present only in x1 - * Loop over x1 - */ - x1c = NULL; - while ((x1c = xml_child_each(x1, x1c, CX_ELMNT)) != NULL){ - if ((yc = xml_spec(x1c)) == NULL){ - clicon_err(OE_UNIX, errno, "Unknown element: %s", xml_name(x1c)); - goto done; - } - /* Does x0 have a child matching x1c? */ - if (match_base_child(x0, x1c, yc, &x0c) < 0) - goto done; - if (x0c == NULL) - if (cxvec_append(x1c, x1vec, x1veclen) < 0) - goto done; - } /* while x0 */ + x0c = xml_child_each(x0, x0c, CX_ELMNT); + x1c = xml_child_each(x1, x1c, CX_ELMNT); + } + ok: retval = 0; done: return retval; @@ -1622,7 +1635,8 @@ xml_default(cxobj *xt, cxobj *xc; cxobj *xb; char *str; - + int added=0; + if ((ys = (yang_stmt*)xml_spec(xt)) == NULL){ retval = 0; goto done; @@ -1637,8 +1651,14 @@ xml_default(cxobj *xt, assert(y->ys_cv); if (!cv_flag(y->ys_cv, V_UNSET)){ /* Default value exists */ if (!xml_find(xt, y->ys_argument)){ + +#ifdef USE_XML_INSERT + if ((xc = xml_new(y->ys_argument, NULL, y)) == NULL) + goto done; +#else if ((xc = xml_new(y->ys_argument, xt, y)) == NULL) goto done; +#endif xml_flag_set(xc, XML_FLAG_DEFAULT); if ((xb = xml_new("body", xc, NULL)) == NULL) goto done; @@ -1650,11 +1670,19 @@ xml_default(cxobj *xt, if (xml_value_set(xb, str) < 0) goto done; free(str); + added++; +#ifdef USE_XML_INSERT + if (xml_insert(xt, xc) < 0) + goto done; +#endif } } } } - xml_sort(xt, NULL); +#ifndef USE_XML_INSERT + if (added) + xml_sort(xt, NULL); +#endif retval = 0; done: return retval; @@ -1946,8 +1974,8 @@ api_path2xpath(yang_stmt *yspec, * @param[out] xpathp Resulting xml tree * @param[out] ypathp Yang spec matching xpathp * @retval 1 OK - * @retval 0 Invalid api_path or associated XML, clicon_err called - * @retval -1 Fatal error, clicon_err called + * @retval 0 Invalid api_path or associated XML, clicon_err called + * @retval -1 Fatal error, clicon_err called * * @note both retval 0 and -1 set clicon_err, but the later is fatal * @see api_path2xpath For api-path to xml xpath translation @@ -2156,8 +2184,8 @@ api_path2xml(char *api_path, goto fail; } nvec--; /* NULL-terminated */ - if ((retval = api_path2xml_vec(vec+1, nvec, - xtop, yspec, nodeclass, + if ((retval = api_path2xml_vec(vec+1, nvec, + xtop, yspec, nodeclass, xbotp, ybotp)) < 1) goto done; xml_yang_root(*xbotp, &xroot); diff --git a/lib/src/clixon_xml_sort.c b/lib/src/clixon_xml_sort.c index c5cc305b..6dd1f3c7 100644 --- a/lib/src/clixon_xml_sort.c +++ b/lib/src/clixon_xml_sort.c @@ -188,19 +188,22 @@ xml_child_spec(cxobj *x, } /*! Help function to qsort for sorting entries in xml child vector same parent - * @param[in] xml object 1 - * @param[in] xml object 2 - * @retval 0 If equal - * @retval <0 if x1 is less than x2 - * @retval >0 if x1 is greater than x2 + * @param[in] x1 object 1 + * @param[in] x2 object 2 + * @param[in] same If set, x1 and x2 are member of same parent & enumeration + * is used + * @retval 0 If equal + * @retval <0 If x1 is less than x2 + * @retval >0 If x1 is greater than x2 * @see xml_cmp1 Similar, but for one object * @note empty value/NULL is smallest value * @note xml_enumerate_children must have been called prior to this call * @note some error cases return as -1 (qsort cant handle errors) */ -static int +int xml_cmp(cxobj *x1, - cxobj *x2) + cxobj *x2, + int same) { yang_stmt *y1; yang_stmt *y2; @@ -218,18 +221,18 @@ xml_cmp(cxobj *x1, int nr2 = 0; cxobj *x1b; cxobj *x2b; - int e; - e=0; if (x1==NULL || x2==NULL) goto done; /* shouldnt happen */ - e=1; y1 = xml_spec(x1); y2 = xml_spec(x2); - nr1 = xml_enumerate_get(x1); - nr2 = xml_enumerate_get(x2); + if (same){ + nr1 = xml_enumerate_get(x1); + nr2 = xml_enumerate_get(x2); + } if (y1==NULL && y2==NULL){ - equal = nr1-nr2; + if (same) + equal = nr1-nr2; goto done; } if (y1==NULL){ @@ -240,24 +243,22 @@ xml_cmp(cxobj *x1, equal = 1; goto done; } - e=2; if (y1 != y2){ yi1 = yang_order(y1); yi2 = yang_order(y2); if ((equal = yi1-yi2) != 0) goto done; } - e=3; /* Now y1==y2, same Yang spec, can only be list or leaf-list, * But first check exceptions, eg config false or ordered-by user * otherwise sort according to key */ if (yang_config(y1)==0 || yang_find(y1, Y_ORDERED_BY, "user") != NULL){ - equal = nr1-nr2; + if (same) + equal = nr1-nr2; goto done; /* Ordered by user or state data : maintain existing order */ } - e=4; switch (y1->ys_keyword){ case Y_LEAF_LIST: /* Match with name and value */ if ((b1 = xml_body(x1)) == NULL) @@ -299,9 +300,8 @@ xml_cmp(cxobj *x1, default: break; } - e=5; done: - clicon_debug(2, "%s %s %s %d %d nr: %d %d yi: %d %d", __FUNCTION__, xml_name(x1), xml_name(x2), equal, e, nr1, nr2, yi1, yi2); + clicon_debug(2, "%s %s %s %d nr: %d %d yi: %d %d", __FUNCTION__, xml_name(x1), xml_name(x2), equal, nr1, nr2, yi1, yi2); return equal; } @@ -312,7 +312,7 @@ static int xml_cmp_qsort(const void* arg1, const void* arg2) { - return xml_cmp(*(struct xml**)arg1, *(struct xml**)arg2); + return xml_cmp(*(struct xml**)arg1, *(struct xml**)arg2, 1); } /*! Compare xml object @@ -531,76 +531,151 @@ xml_search(cxobj *x0, { cxobj *xa; int low = 0; - int high = xml_child_nr(x0); + int upper = xml_child_nr(x0); /* Assume if there are any attributes, they are first in the list, mask them by raising low to skip them */ - for (low=0; low upper){ /* beyond range */ + clicon_err(OE_XML, 0, "low>upper %d %d", low, upper); + goto done; + } + if (low == upper){ + retval = low; + goto done; + } + mid = (low + upper) / 2; + if (mid >= xml_child_nr(xp)){ /* beyond range */ + clicon_err(OE_XML, 0, "Beyond range %d %d %d", low, mid, upper); + goto done; + } + xc = xml_child_i(xp, mid); + if ((yc = xml_spec(xc)) == NULL){ + clicon_err(OE_XML, 0, "No spec found %s", xml_name(xc)); + goto done; + } + if (yc == yn){ /* Same yang */ + if (userorder){ /* append: increment linearly until no longer equal */ + for (i=mid+1; i, xn = + * same order but different yang spec + */ + } + if (low +1 == upper){ /* termination criterium */ +#if 0 + if (xml_child_nr(xp) <= mid+1){ + retval = mid; + goto done; + } +#endif + if (cmp<0) { + retval = mid; + goto done; + } + retval = mid+1; + goto done; + } + if (cmp == 0){ + retval = mid; + goto done; + } + else if (cmp < 0) + return xml_insert2(xp, xn, yn, yni, userorder, low, mid); + else + return xml_insert2(xp, xn, yn, yni, userorder, mid+1, upper); + done: + return retval; +} + +/*! Insert xc as child to xp in sorted place. Remove xc from previous parent. + * @param[in] xp Parent xml node. If NULL just remove from old parent. + * @param[in] x Child xml node to insert under xp + * @retval 0 OK + * @retval -1 Error + * @see xml_addsub where xc is appended. xml_insert is xml_addsub();xml_sort() */ int -xml_insert_pos(cxobj *x0, - char *name, - int yangi, - enum rfc_6020 keyword, - int keynr, - char **keyvec, - char **keyval, - int low, - int upper) +xml_insert(cxobj *xp, + cxobj *xi) { - int mid; - cxobj *xc; + int retval = -1; + cxobj *xa; + int low = 0; + int upper; yang_stmt *y; - int cmp; - int i; int userorder= 0; - - if (upper < low) - return low; /* not found */ - mid = (low + upper) / 2; - if (mid >= xml_child_nr(x0)) - return xml_child_nr(x0); /* upper range */ - xc = xml_child_i(x0, mid); - y = xml_spec(xc); - cmp = yangi-yang_order(y); - if (cmp == 0){ - cmp = xml_cmp1(xc, y, name, keyword, keynr, keyvec, keyval, keycvec, &userorder); - if (userorder){ /* Look inside this yangi order */ - /* Special case: append last of equals if ordered by user */ - for (i=mid+1;iys_keyword == Y_LIST || y->ys_keyword == Y_LEAF_LIST) + userorder = (yang_find(y, Y_ORDERED_BY, "user") != NULL); + yi = yang_order(y); + if ((i = xml_insert2(xp, xi, y, yi, userorder, low, upper)) < 0) + goto done; + if (xml_child_insert_pos(xp, xi, i) < 0) + goto done; + xml_parent_set(xi, xp); + retval = 0; + done: + return retval; } -#endif /* NOTUSED */ /*! Verify all children of XML node are sorted according to xml_sort() * @param[in] x XML node. Check its children @@ -626,7 +701,7 @@ xml_sort_verify(cxobj *x0, xml_enumerate_children(x0); while ((x = xml_child_each(x0, x, -1)) != NULL) { if (xprev != NULL){ /* Check xprev <= x */ - if (xml_cmp(xprev, x) > 0) + if (xml_cmp(xprev, x, 1) > 0) goto done; } xprev = x; diff --git a/lib/src/clixon_yang.c b/lib/src/clixon_yang.c index 015c4476..e3effab4 100644 --- a/lib/src/clixon_yang.c +++ b/lib/src/clixon_yang.c @@ -660,6 +660,30 @@ yang_choice(yang_stmt *y) return NULL; } +static int +order1_choice(yang_stmt *yp, + yang_stmt *y) +{ + yang_stmt *ys; + yang_stmt *yc; + int i; + int j; + + for (i=0; iys_len; i++){ + ys = yp->ys_stmt[i]; + if (ys->ys_keyword == Y_CASE){ + for (j=0; jys_len; j++){ + yc = ys->ys_stmt[j]; + if (yang_datanode(yc) && yc == y) + return 1; + } + } + else if (yang_datanode(ys) && ys == y) + return 1; + } + return 0; +} + /*! Find matching y in yp:s children, return 0 and index or -1 if not found. * @param[in] yp Parent * @param[in] y Yang datanode to find @@ -677,10 +701,16 @@ order1(yang_stmt *yp, for (i=0; iys_len; i++){ ys = yp->ys_stmt[i]; - if (!yang_datanode(ys)) - continue; - if (ys==y) - return 1; + if (ys->ys_keyword == Y_CHOICE){ + if (order1_choice(ys, y) == 1) /* If one of the choices is "y" */ + return 1; + } + else { + if (!yang_datanode(ys)) + continue; + if (ys==y) + return 1; + } (*index)++; } return 0; @@ -703,7 +733,14 @@ yang_order(yang_stmt *y) int j=0; int tot = 0; + /* Some special handling if yp is choice (or case) and maybe union? + * if so, the real parent (from an xml point of view) is the parents + * parent. + */ yp = y->ys_parent; + while (yp->ys_keyword == Y_CASE || yp->ys_keyword == Y_CHOICE) + yp = yp->ys_parent; + /* XML nodes with yang specs that are children of modules are special - * In clixon, they are seen as an "implicit" container where the XML can come from different * modules. The order must therefore be global among yang top-symbols to be unique. @@ -1890,6 +1927,7 @@ yang_parse_str(char *str, * @param[in] ysp Yang specification. Should have been created by caller using yspec_new * @retval ymod Top-level yang (sub)module * @retval NULL Error + * @note this function simply parse a yang spec, no dependencies or checks */ yang_stmt * yang_parse_file(int fd, diff --git a/test/README.md b/test/README.md index d11a62ef..809f00db 100644 --- a/test/README.md +++ b/test/README.md @@ -60,6 +60,10 @@ The `mem.sh` runs memory checks using valgrind. Start it with no arguments to te mem.sh restconf backend # Only backend and cli ``` +## Performance plots + +The script `plot_perf.sh` produces gnuplots for some testcases. + ## Site.sh You may add your site-specific modifications in a `site.sh` file. Example: ``` diff --git a/test/plot_perf.sh b/test/plot_perf.sh index dc565817..80c17c73 100755 --- a/test/plot_perf.sh +++ b/test/plot_perf.sh @@ -1,10 +1,33 @@ #!/bin/bash # Transactions per second for large lists read/write plotter using gnuplot -# WORK IN PROGRESS -. ./lib.sh -max=2000 # Nr of db entries -step=200 -reqs=500 +# What do I want to plot? +# 1. How long to write 100K entries? +# - netconf / restconf +# - list / leaf-list +# 2. How long to read 100K entries? +# - netconf/ restconf +# - list / leaf-list +# 3. How long to commit 100K entries? (netconf) +# - list / leaf-list +# 4. In database 100K entries. How many read operations per second? +# - netconf/ restconf +# - list / leaf-list +# 5. 100K entries. How many write operations per second? +# - netconf / restconf +# - list / leaf-list +# 6. 100K entries. How may delete operations per second? +# - netconf / restconf +# - list / leaf-list + +# Magic line must be first in script (see README.md) +s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi + +#max=2000 # Nr of db entries +#step=200 +#reqs=500 + +# Global variables +APPNAME=example cfg=$dir/plot-conf.xml fyang=$dir/plot.yang fconfig=$dir/config @@ -15,7 +38,10 @@ fconfig=$dir/config clixon_netconf=clixon_netconf cat < $fyang -module ietf-ip{ +module scaling{ + yang-version 1.1; + namespace "urn:example:clixon"; + prefix sc; container x { list y { key "a"; @@ -34,23 +60,32 @@ module ietf-ip{ EOF cat < $cfg - + $cfg - $fyang - ietf-ip - /usr/local/var/routing/routing.sock - /usr/local/var/routing/routing.pidfile -false - /usr/local/var/routing - + $dir + /usr/local/share/clixon + scaling + /usr/local/var/$APPNAME/$APPNAME.sock + /usr/local/var/example/$APPNAME.pidfile + false + $dir + false + EOF -run(){ +# Run function +# args: +# where mode is one of: +# readlist writelist restreadlist restwritelist +runfn(){ nr=$1 # Number of entries in DB reqs=$2 - mode=$3 + operation=$3 - echo -n "replace" > $fconfig +# echo "runfn nr=$nr reqs=$reqs mode=$mode" + +# new "generate config with $nr list entries" + echo -n "replace" > $fconfig for (( i=0; i<$nr; i++ )); do case $mode in readlist|writelist|restreadlist|restwritelist) @@ -64,100 +99,135 @@ run(){ echo "]]>]]>" >> $fconfig +# new "netconf write $nr entry to backend" expecteof_file "$clixon_netconf -qf $cfg -y $fyang" "$fconfig" "^]]>]]>$" case $mode in readlist) +# new "netconf GET list $reqs" time -p for (( i=0; i<$reqs; i++ )); do rnd=$(( ( RANDOM % $nr ) )) echo "]]>]]>" done | $clixon_netconf -qf $cfg -y $fyang > /dev/null ;; - writelist) + writelist) +# new "netconf WRITE list $reqs" time -p for (( i=0; i<$reqs; i++ )); do rnd=$(( ( RANDOM % $nr ) )) - echo "$rnd$rnd]]>]]>" + echo "$rnd$rnd]]>]]>" done | $clixon_netconf -qf $cfg -y $fyang > /dev/null ;; restreadlist) +# new "restconf GET list $reqs" time -p for (( i=0; i<$reqs; i++ )); do rnd=$(( ( RANDOM % $nr ) )) - curl -sSG http://localhost/restconf/data/x/y=$rnd,$rnd > /dev/null + curl -sSG http://localhost/restconf/data/scaling:x/y=$rnd > /dev/null done ;; writeleaflist) +# new "netconf GET leaf-list $reqs" time -p for (( i=0; i<$reqs; i++ )); do rnd=$(( ( RANDOM % $nr ) )) - echo "$rnd]]>]]>" + echo "$rnd]]>]]>" done | $clixon_netconf -qf $cfg -y $fyang > /dev/null ;; esac +# new "discard test" expecteof "$clixon_netconf -qf $cfg -y $fyang" "]]>]]>" "^]]>]]>$" } -step(){ +# Step +# args: +stepfn(){ i=$1 - mode=$2 + reqs=$2 + mode=$3 + +>&2 echo "stepfn $mode: i=$i reqs=$reqs" echo -n "" > $fconfig - t=$(TEST=%e run $i $reqs $mode 2>&1 | awk '/real/ {print $2}') - #TEST=%e run $i $reqs $mode 2>&1 + t=$(TEST=%e runfn $i $reqs $mode 2>&1 | awk '/real/ {print $2}') + #TEST=%e runfn $i $reqs $mode 2>&1 # t is time in secs of $reqs -> transactions per second. $reqs p=$(echo "$reqs/$t" | bc -lq) # p is transactions per second. + # write to gnuplot file: $dir/$mode echo "$i $p" >> $dir/$mode -# echo "m:$mode i:$i t=$t p=$p" } +# Run once +#args: once(){ - # kill old backend (if any) - sudo clixon_backend -zf $cfg -y $fyang - if [ $? -ne 0 ]; then - err + # Input Parameters + step=$1 + reqs=$2 + max=$3 + + echo "oncefn step=$step reqs=$reqs max=$max" + new "test params: -f $cfg -y $fyang" + if [ $BE -ne 0 ]; then + new "kill old backend" + sudo clixon_backend -zf $cfg -y $fyang + if [ $? -ne 0 ]; then + err + fi + new "start backend -s init -f $cfg -y $fyang" + start_backend -s init -f $cfg -y $fyang fi - # start new backend - sudo clixon_backend -s init -f $cfg -y $fyang - if [ $? -ne 0 ]; then - err - fi + new "kill old restconf daemon" + sudo pkill -u www-data -f "/www-data/clixon_restconf" - # Always as a start + new "start restconf daemon" + start_restconf -f $cfg -y $fyang + + new "waiting" + sleep $RCWAIT + + new "Intial steps as start" for (( i=10; i<=$step; i=i+10 )); do - step $i readlist - step $i writelist - step $i restreadlist - step $i writeleaflist + stepfn $i $reqs readlist + stepfn $i $reqs writelist + stepfn $i $reqs restreadlist + stepfn $i $reqs writeleaflist done - # Actual steps + rnd=$(( ( RANDOM % $step ) )) + echo "curl -sSG http://localhost/restconf/data/scaling:x/y=$rnd" + curl -sSG http://localhost/restconf/data/scaling:x/y=$rnd +exit + new "Actual steps" for (( i=$step; i<=$max; i=i+$step )); do - step $i readlist - step $i writelist - step $i restreadlist - step $i writeleaflist + stepfn $i $reqs readlist + stepfn $i $reqs writelist + stepfn $i $reqs restreadlist + stepfn $i $reqs writeleaflist done - # Check if still alive - pid=`pgrep clixon_backend` - if [ -z "$pid" ]; then - err "backend already dead" - fi - # kill backend - sudo clixon_backend -zf $cfg - if [ $? -ne 0 ]; then - err "kill backend" + new "Kill restconf daemon" + stop_restconf + + if [ $BE -ne 0 ]; then + new "Kill backend" + # Check if premature kill + pid=`pgrep -u root -f clixon_backend` + if [ -z "$pid" ]; then + err "backend already dead" + fi + # kill backend + stop_backend -f $cfg fi } -once +# step=200 reqs=500 max=2000 +once 200 500 1000 gnuplot -persist < $cfg + + $cfg + $dir + /usr/local/share/clixon + /usr/local/lib/$APPNAME/clispec + /usr/local/lib/$APPNAME/cli + $APPNAME + /usr/local/var/$APPNAME/$APPNAME.sock + /usr/local/var/$APPNAME/$APPNAME.pidfile + $dir + true + +EOF + +cat < $fyang +module example { + yang-version 1.1; + namespace "urn:example:example"; + prefix ex; + revision 2019-01-13; + container c{ + list a{ + key x; + leaf x{ + type int32; + } + } + } +} +EOF + +testrun(){ + x0=$1 + xi="$2" + xp=c + + new "random list add leaf-list" + # First run sorted (assume this is the refernce == correct) + rs=$($clixon_util_insert -y $fyang -x "$xi" -b "$x0" -p $xp $OPTS -s) + # Then run actual insert + r0=$($clixon_util_insert -y $fyang -x "$xi" -b "$x0" -p $xp $OPTS) + # If both are null something is amiss + if [ -z "$r0" -a -z "$rs" ]; then + err "length of retval is zero" + fi + # echo "rs:$rs" +# echo "r0:$r0" + # Check they are equal + if [[ "$r0" != "$rs" ]]; then + err "$rs" "$r0" + fi +} + +new "test params: -y $fyang $OPTS" + +# Empty element base list +x0='' +new "empty list" +testrun "$x0" "1" + +# One element base list +x0='99' +new "one element list first" +testrun "$x0" "1" + +new "one element list last" +testrun "$x0" "100" + +# Two element base list +x0='299' +new "two element list first" +testrun "$x0" "1" + +new "two element list mid" +testrun "$x0" "12" + +new "two element list last" +testrun "$x0" "3000" + +# Three element base list +x0='299101' +new "three element list first" +testrun "$x0" "1" + +new "three element list second" +testrun "$x0" "10" + +new "three element list third" +testrun "$x0" "100" + +new "three element list last" +testrun "$x0" "1000" + +# Four element base list +x0='299101200' + +new "four element list first" +testrun "$x0" "1" + +new "four element list second" +testrun "$x0" "10" + +new "four element list third" +testrun "$x0" "100" + +new "four element list fourth" +testrun "$x0" "102" + +new "four element list last" +testrun "$x0" "1000" + +# Five element base list +x0='299101200300' + +new "five element list first" +testrun "$x0" "1" + +new "five element list mid" +testrun "$x0" "100" + +new "five element list last" +testrun "$x0" "1000" + +cat < $fyang +module example { + yang-version 1.1; + namespace "urn:example:example"; + prefix ex; + revision 2019-01-13; + container c{ + leaf a{ + type string; + } + container b{ + leaf a { + type string; + } + } + choice c1{ + case a{ + leaf x{ + type string; + } + } + case b{ + leaf y{ + type int32; + } + } + } + choice c2{ + leaf z{ + type string; + } + leaf t{ + type int32; + } + } + list d{ + key x; + leaf x{ + type int32; + } + ordered-by user; + } + leaf-list e{ + type int32; + } + } +} +EOF + +# Advanced list +# Empty base list +x0='' +xp=c +new "adv empty list add leaf" +testrun "$x0" "leaf" + +new "adv empty list add choice c1" +testrun "$x0" "choice1" + +xi='33' +new "adv empty list add leaf-list" +testrun "$x0" "33" + +# base list +x0='leafchoice133' + +new "adv list add leaf-list" +testrun "$x0" "32" + +new "adv list add leaf-list" +testrun "$x0" "32" + +rm -rf $dir diff --git a/test/test_perf.sh b/test/test_perf.sh index 596d7b70..e3513a52 100755 --- a/test/test_perf.sh +++ b/test/test_perf.sh @@ -5,7 +5,7 @@ s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi # Number of list/leaf-list entries in file -: ${perfnr:=1000} +: ${perfnr:=2000} # Number of requests made get/put : ${perfreq:=100} @@ -15,6 +15,7 @@ APPNAME=example cfg=$dir/scaling-conf.xml fyang=$dir/scaling.yang fconfig=$dir/large.xml +fconfig2=$dir/large2.xml cat < $fyang module scaling{ @@ -43,12 +44,11 @@ cat < $cfg $cfg $dir /usr/local/share/clixon - $IETFRFC scaling /usr/local/var/$APPNAME/$APPNAME.sock - /usr/local/var/$APPNAME/$APPNAME.pidfile + /usr/local/var/example/$APPNAME.pidfile false - /usr/local/var/$APPNAME + $dir false EOF @@ -90,9 +90,6 @@ expecteof_file "/usr/bin/time -f %e $clixon_netconf -qf $cfg -y $fyang" "$fconfi new "netconf write large config again" expecteof_file "/usr/bin/time -f %e $clixon_netconf -qf $cfg -y $fyang" "$fconfig" "^]]>]]>$" -# Remove the file, its used for different purposes further down -rm $fconfig - # Now commit it from candidate to running new "netconf commit large config" expecteof "/usr/bin/time -f %e $clixon_netconf -qf $cfg -y $fyang" 0 "]]>]]>" "^]]>]]>$" @@ -119,7 +116,7 @@ done | $clixon_netconf -qf $cfg -y $fyang > /dev/null new "restconf get $perfreq small config" time -p for (( i=0; i<$perfreq; i++ )); do rnd=$(( ( RANDOM % $perfnr ) )) - curl -sG http://localhost/restconf/data/scaling:x/y=$rnd,$rnd > /dev/null + curl -sG http://localhost/restconf/data/scaling:x/y=$rnd > /dev/null done new "restconf add $perfreq small config" @@ -135,19 +132,23 @@ expecteof "/usr/bin/time -f %e $clixon_netconf -qf $cfg -y $fyang" 0 "]]>]]>" '^{"data": {"scaling:x": {"y": \[{"a": 0,"b": 0},{ "a": 1,"b": 1},{ "a": 2,"b": 2},{ "a": 3,"b": 3},' +new "restconf delete $perfreq small config" +time -p for (( i=0; i<$perfreq; i++ )); do + rnd=$(( ( RANDOM % $perfnr ) )) + curl -s -X DELETE http://localhost/restconf/data/scaling:x/y=$rnd +done + # Now do leaf-lists istead of leafs new "generate large leaf-list config" -echo -n "replace" > $fconfig +echo -n "replace" > $fconfig2 for (( i=0; i<$perfnr; i++ )); do - echo -n "$i" >> $fconfig + echo -n "$i" >> $fconfig2 done -echo "]]>]]>" >> $fconfig +echo "]]>]]>" >> $fconfig2 new "netconf replace large list-leaf config" -expecteof_file "/usr/bin/time -f %e $clixon_netconf -qf $cfg -y $fyang" "$fconfig" "^]]>]]>$" - -rm $fconfig +expecteof_file "/usr/bin/time -f %e $clixon_netconf -qf $cfg -y $fyang" "$fconfig2" "^]]>]]>$" new "netconf commit large leaf-list config" expecteof "/usr/bin/time -f %e $clixon_netconf -qf $cfg -y $fyang" 0 "]]>]]>" "^]]>]]>$" diff --git a/util/Makefile.in b/util/Makefile.in index a4ef9f67..a7eacd4c 100644 --- a/util/Makefile.in +++ b/util/Makefile.in @@ -73,6 +73,7 @@ APPSRC += clixon_util_json.c APPSRC += clixon_util_yang.c APPSRC += clixon_util_xpath.c APPSRC += clixon_util_datastore.c +APPSRC += clixon_util_insert.c ifeq ($(with_restconf),yes) APPSRC += clixon_util_stream.c # Needs curl endif @@ -107,6 +108,9 @@ clixon_util_stream: clixon_util_stream.c $(LIBDEPS) clixon_util_datastore: clixon_util_datastore.c $(LIBDEPS) $(CC) $(INCLUDES) $(CPPFLAGS) @CFLAGS@ $(LDFLAGS) $^ $(LIBS) -o $@ +clixon_util_insert: clixon_util_insert.c $(LIBDEPS) + $(CC) $(INCLUDES) $(CPPFLAGS) @CFLAGS@ $(LDFLAGS) $^ $(LIBS) -o $@ + distclean: clean rm -f Makefile *~ .depend diff --git a/util/clixon_util_insert.c b/util/clixon_util_insert.c new file mode 100644 index 00000000..1edf35f7 --- /dev/null +++ b/util/clixon_util_insert.c @@ -0,0 +1,215 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + +See https://www.w3.org/TR/xpath/ + + * Turn this on to get an xpath test program + * Usage: xpath [] + * read xpath on first line and xml on rest of lines from input + * Example compile: + gcc -g -o xpath -I. -I../clixon ./clixon_xsl.c -lclixon -lcligen + * Example run: +echo "a\n" | xpath +*/ + +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* cligen */ +#include + +/* clixon */ +#include "clixon/clixon.h" + +static int +usage(char *argv0) +{ + fprintf(stderr, "usage:%s [options]\n" + "where options are\n" + "\t-h \t\tHelp\n" + "\t-D \tDebug\n" + "\t-y \tYANG spec file (or stdin)\n" + "\t-b \tXML base expression\n" + "\t-x \tXML to insert\n" + "\t-p \tXpath to where in base and XML\n" + "\t-s \tSort output after insert\n" + "Assume insert xml is first child of xpath. Ie if xml=23 and xpath=a, then inserted element is 23\n", + argv0 + ); + exit(0); +} + +int +main(int argc, char **argv) +{ + int retval = -1; + char *argv0 = argv[0]; + int c; + char *filename = NULL; + int fd = 0; /* unless overriden by argv[1] */ + char *x0str = NULL; + char *xistr = NULL; + char *xpath = NULL; + yang_stmt *yspec; + cxobj *x0 = NULL; + cxobj *xb; + cxobj *xi = NULL; + int sort = 0; + clicon_handle h; + + clicon_log_init("clixon_insert", LOG_DEBUG, CLICON_LOG_STDERR); + if ((h = clicon_handle_init()) == NULL) + goto done; + optind = 1; + opterr = 0; + while ((c = getopt(argc, argv, "hD:y:b:x:p:s")) != -1) + switch (c) { + case 'h': + usage(argv0); + break; + case 'D': + if (sscanf(optarg, "%d", &debug) != 1) + usage(argv0); + break; + case 'y': /* YANG spec file */ + filename = optarg; + if (0 && (fd = open(filename, O_RDONLY)) < 0){ + clicon_err(OE_UNIX, errno, "open(%s)", argv[1]); + goto done; + } + break; + case 'b': /* Base XML expression */ + x0str = optarg; + break; + case 'x': /* XML to insert */ + xistr = optarg; + break; + case 'p': /* XPATH base */ + xpath = optarg; + break; + case 's': /* sort output after insert */ + sort++; + break; + default: + usage(argv[0]); + break; + } + if (xistr == NULL || x0str == NULL) + usage(argv0); + if (xpath == NULL) + usage(argv0); + if (filename == NULL) + usage(argv0); + clicon_debug(1, "xistr:%s", xistr); + clicon_debug(1, "x0str:%s", x0str); + clicon_debug(1, "xpath:%s", xpath); + if ((yspec = yspec_new()) == NULL) + goto done; +#if 1 + if (yang_spec_parse_file(h, filename, yspec) < 0) + goto done; +#else + if (yang_parse_file(fd, "yang test", yspec) == NULL) + goto done; +#endif + /* Parse base XML */ + if (xml_parse_string(x0str, yspec, &x0) < 0){ + clicon_err(OE_XML, 0, "Parsing base xml: %s", x0str); + goto done; + } + if (xml_apply(x0, CX_ELMNT, xml_spec_populate, yspec) < 0) + goto done; + if ((xb = xpath_first(x0, "%s", xpath)) == NULL){ + clicon_err(OE_XML, 0, "xpath: %s not found in x0", xpath); + goto done; + } + if (debug){ + clicon_debug(1, "xb:"); + xml_print(stderr, xb); + } + /* Parse insert XML */ + if (xml_parse_string(xistr, yspec, &xi) < 0){ + clicon_err(OE_XML, 0, "Parsing insert xml: %s", xistr); + goto done; + } + if (xml_apply(xi, CX_ELMNT, xml_spec_populate, yspec) < 0) + goto done; + if ((xi = xpath_first(xi, "%s", xpath)) == NULL){ + clicon_err(OE_XML, 0, "xpath: %s not found in xi", xpath); + goto done; + } + /* Find first element child */ + if ((xi = xml_child_i_type(xi, 0, CX_ELMNT)) == NULL){ + clicon_err(OE_XML, 0, "xi has no element child"); + goto done; + } + /* Remove it from parent */ + if (xml_rm(xi) < 0) + goto done; + if (debug){ + clicon_debug(1, "xi:"); + xml_print(stderr, xi); + } + if (xml_insert(xb, xi) < 0) + goto done; + if (debug){ + clicon_debug(1, "x0:"); + xml_print(stderr, x0); + } + if (sort) + xml_sort(xb, NULL); + clicon_xml2file(stdout, xb, 0, 0); + + retval = 0; + done: + if (x0) + xml_free(x0); + if (yspec) + yspec_free(yspec); + if (fd > 0) + close(fd); + return retval; +} From d46ca41c8b66c19c1d9442db45648df42d918725 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Tue, 16 Apr 2019 12:09:21 +0200 Subject: [PATCH 02/13] Further optimizions and bugfixing of that --- README.md | 1 + doc/large-lists.md | 134 ++++++++++++ docker/system/README.md | 7 + lib/src/clixon_xml.c | 2 +- lib/src/clixon_xml_map.c | 8 +- lib/src/clixon_xml_sort.c | 273 +++++++----------------- lib/src/clixon_yang_module.c | 8 +- test/lib.sh | 4 +- test/plot_perf.sh | 393 ++++++++++++++++++++++------------- test/test_restconf.sh | 2 +- test/test_restconf2.sh | 2 +- 11 files changed, 482 insertions(+), 352 deletions(-) create mode 100644 doc/large-lists.md diff --git a/README.md b/README.md index af2b3b3b..38385af8 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ support. * [Runtime](#runtime) * [Clixon project page](http://www.clicon.org) * [Tests and CI](test/README.md) + * [Scaling: large lists](doc/large-lists.md) * [Containers](docker/README.md) * [Roadmap](doc/ROADMAP.md) * [Reference manual](#reference) diff --git a/doc/large-lists.md b/doc/large-lists.md new file mode 100644 index 00000000..1b1b15a4 --- /dev/null +++ b/doc/large-lists.md @@ -0,0 +1,134 @@ +# Large lists in Clixon + + * [Background](#background) + * [Overview](#overview) + * [Test descriptions]#test-descriptions) + +## Background + +Clixon is a configuration management tool. In this paper the case of +a large number of "flat" list and leaf-list entries are investigated. +There may be other scaling usecases, such as large configuratin +"depth", large number of requesting clients, etc. However, these are +not investigated here. + +## Overview +The basic case is a large list, according to the following Yang specification: +``` + list y { + key "a"; + leaf a { + type int32; + } + leaf b { + type string; + } + } +``` +where `a` is a unique key and `b` is a payload, useful in replace operations. + +There is also a leaf-list as follows: +``` + leaf-list c { + type string; + } +``` + +XML lists with `N` elements are generated based on +this configuration, eg for `N=10`: +``` + 00 + 11 + 22 + 33 + 44 + 55 + 66 + 77 + 88 + 99 +``` + +Requests are made using a random function, a request on the list above will on the form: +``` + curl -G http://localhost/restconf/data/y=(rnd%$N) +``` + +## Test descriptions + +### Limitations + +Test were not made using CLI interaction. + +### Setup + +The setup consisted of the following components running on the same machine: +* A clixon backend daemon +* A clixon restconf daemon +* An nginx daemon daemon +* A netconf client program +* curl client +* A bash terminal and test script [plot_perf.sh](../test/plot_perf.sh) +* Gnuplot for generating plots + +### Config file +The following Clixon config file was used: +``` + + $cfg + $dir + /usr/local/share/clixon + scaling + /usr/local/var/example/example.sock + /usr/local/var/example/example.pidfile + false + $dir + false + +``` +where `$dir` and `$cfg`are local files. For more info see [plot_perf.sh]. + +### Testcases + +All tests measure the "real" time of a command on a lightly loaded +machine using the Linux command `time(1)`. + +The following tests were made (for each architecture and protocol): +* Write `N` entries in one single operation. (With an empty datastore) +* Read `N` entries in one single operation. (With a datastore of `N` entries) +* Commit `N` entries (With a candidate of `N` entries and empty running) +* Read 1 entry (In a datastore of `N` entries) +* Write/Replace 1 entry (In a datastore of `N` entries) +* Delete 1 entry (In a datastore of `N` entries) + +### Protocols + +The tests are made using: +* Netconf[RFC6241] and +* Restconf[RFC8040]. +Notably, CLI tests are for future study. + +### Architectures + +The tests were made on the following hardware, all running Ubuntu Linux: +* [i686] dual Intel Core Duo processor (IBM Thinkpad X60), 3GB memory +* arm 32-bit (Raspberry PI 3) +* x86 64-bit (Intel NUC) + +### Operating systems + +On i686: +``` +Linux version 4.4.0-143-generic (buildd@lgw01-amd64-037) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.10) ) #169-Ubuntu SMP Thu Feb 7 07:56:51 UTC 2019 +``` + +## Results + +## References + +[RFC6241](https://tools.ietf.org/html/rfc6241) "Network Configuration Protocol (NETCONF)" +[RFC8040](https://tools.ietf.org/html/rfc8040) "RESTCONF Protocol" +[i686](https://ark.intel.com/content/www/us/en/ark/products/27235/intel-core-duo-processor-t2400-2m-cache-1-83-ghz-667-mhz-fsb.html) +[plot_perf.sh](../test/plot_perf.sh) Test script + + diff --git a/docker/system/README.md b/docker/system/README.md index 73378264..61febc54 100644 --- a/docker/system/README.md +++ b/docker/system/README.md @@ -54,3 +54,10 @@ To check status and then kill it: ``` You trigger the test scripts inside the container using `make test`. + +## Changing code + +If you want to edit clixon code so it runs in the container? +You either +(1) "persistent": make your changes in the actual clixon code and commit; make clean to remove the local clone; make test again +(2) "volatile" edit the local clone; make test. \ No newline at end of file diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index 4a88311a..b19909a6 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -814,7 +814,7 @@ xml_cv_set(cxobj *x, * name "name". * * @param[in] x_up Base XML object - * @param[in] name shell wildcard pattern to match with node name + * @param[in] name Node name * * @retval xmlobj if found. * @retval NULL if no such node found. diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index 3154d18b..0f569048 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -2021,6 +2021,7 @@ api_path2xml_vec(char **vec, cxobj *x = NULL; yang_stmt *y = NULL; yang_stmt *ymod; + yang_stmt *ykey; char *namespace = NULL; if ((nodeid = vec[0]) == NULL || strlen(nodeid)==0){ @@ -2104,7 +2105,12 @@ api_path2xml_vec(char **vec, /* Create keys */ while ((cvi = cvec_each(cvk, cvi)) != NULL) { keyname = cv_string_get(cvi); - if ((xn = xml_new(keyname, x, NULL)) == NULL) + if ((ykey = yang_find(y, Y_LEAF, keyname)) == NULL){ + clicon_err(OE_XML, 0, "List statement \"%s\" has no key leaf \"%s\"", + yang_argument_get(y), keyname); + goto done; + } + if ((xn = xml_new(keyname, x, ykey)) == NULL) goto done; xml_type_set(xn, CX_ELMNT); if ((xb = xml_new("body", xn, NULL)) == NULL) diff --git a/lib/src/clixon_xml_sort.c b/lib/src/clixon_xml_sort.c index 3d9c5bbf..7cfd6d70 100644 --- a/lib/src/clixon_xml_sort.c +++ b/lib/src/clixon_xml_sort.c @@ -89,7 +89,8 @@ xml_cv_cache(cxobj *x, uint8_t fraction = 0; char *body; - body = xml_body(x); + if ((body = xml_body(x)) == NULL) + body=""; if ((cv = xml_cv(x)) != NULL) goto ok; if ((y = xml_spec(x)) == NULL) @@ -191,13 +192,21 @@ xml_child_spec(cxobj *x, * @param[in] x1 object 1 * @param[in] x2 object 2 * @param[in] same If set, x1 and x2 are member of same parent & enumeration - * is used + * is used (see explanation below) * @retval 0 If equal * @retval <0 If x1 is less than x2 * @retval >0 If x1 is greater than x2 * @see xml_cmp1 Similar, but for one object + * + * There are distinct calls for this function: + * 1. For sorting in an existing list of XML children + * 2. For searching of an existing element in a list + * In the first case, there is a special case for "ordered-by-user", where + * if they have the same yang-spec, the existing order is used as tie-breaker. + * In other words, if order-by-system, or if the case (2) above, the existing + * order is ignored and the actual xml element contents is examined. * @note empty value/NULL is smallest value - * @note xml_enumerate_children must have been called prior to this call + * @note some error cases return as -1 (qsort cant handle errors) * @note some error cases return as -1 (qsort cant handle errors) */ int @@ -252,13 +261,15 @@ xml_cmp(cxobj *x1, /* Now y1==y2, same Yang spec, can only be list or leaf-list, * But first check exceptions, eg config false or ordered-by user * otherwise sort according to key + * If the two elements are in the same list, and they are ordered-by user + * then do not look more into equivalence, use the enumeration in the + * existing list. */ - if (yang_config(y1)==0 || - yang_find(y1, Y_ORDERED_BY, "user") != NULL){ - if (same) + if (same && + (yang_config(y1)==0 || yang_find(y1, Y_ORDERED_BY, "user") != NULL)){ equal = nr1-nr2; - goto done; /* Ordered by user or state data : maintain existing order */ - } + goto done; /* Ordered by user or state data : maintain existing order */ + } switch (y1->ys_keyword){ case Y_LEAF_LIST: /* Match with name and value */ if ((b1 = xml_body(x1)) == NULL) @@ -289,8 +300,10 @@ xml_cmp(cxobj *x1, else{ if (xml_cv_cache(x1b, &cv1) < 0) /* error case */ goto done; + assert(cv1); if (xml_cv_cache(x2b, &cv2) < 0) /* error case */ goto done; + assert(cv2); if ((equal = cv_cmp(cv1, cv2)) != 0) goto done; } @@ -315,92 +328,6 @@ xml_cmp_qsort(const void* arg1, return xml_cmp(*(struct xml**)arg1, *(struct xml**)arg2, 1); } -/*! Compare xml object - * @param[in] x XML node to compare with - * @param[in] y The yang spec of x - * @param[in] name Name to compare with x - * @param[in] keyword Yang keyword (stmt type) to compare w x/y - * @param[in] keynr Length of keyvec/keyval vector when applicable - * @param[in] keyvec Array of of yang key identifiers - * @param[in] keyval Array of of yang key values - * @param[out] userorder If set, this yang order is user ordered, linear search - * @retval 0 If equal (or userorder set) - * @retval <0 if arg1 is less than arg2 - * @retval >0 if arg1 is greater than arg2 - * @see xml_cmp Similar, but for two objects - * @note Does not care about y type of value as xml_cmp - */ -static int -xml_cmp1(cxobj *x, - yang_stmt *y, - char *name, - enum rfc_6020 keyword, - int keynr, - char **keyvec, - char **keyval, - cg_var **keycvec, - int *userorder) -{ - char *b; - cxobj *xb; - int i; - char *keyname; - char *key; - int match = 0; - cg_var *cv; - - /* state data = userorder */ - if (userorder && yang_config(y)==0) - *userorder=1; - /* Check if same yang spec (order in yang stmt list) */ - switch (keyword){ - case Y_CONTAINER: /* Match with name */ - case Y_LEAF: /* Match with name */ - match = strcmp(name, xml_name(x)); - break; - case Y_LEAF_LIST: /* Match with name and value */ - if (userorder && yang_find(y, Y_ORDERED_BY, "user") != NULL) - *userorder=1; - if ((b=xml_body(x)) == NULL) - match = 1; - else{ - if (keycvec[0]){ - if (xml_cv_cache(x, &cv) < 0) /* error case */ - goto done; - match = cv_cmp(keycvec[0], cv); - } - else - match = strcmp(keyval[0], b); - } - break; - case Y_LIST: /* Match with array of key values */ - if (userorder && yang_find(y, Y_ORDERED_BY, "user") != NULL) - *userorder=1; - /* All must match */ - for (i=0; i=0; i--){ /* Then decrement */ - xc = xml_child_i(x0, i); + xc = xml_child_i(xp, i); y = xml_spec(xc); if (yangi!=yang_order(y)) break; - if (xml_cmp1(xc, y, name, keyword, keynr, keyvec, keyval, keycvec, NULL) == 0) + if (xml_cmp(xc, x1, 0) == 0) return xc; } return NULL; /* Not found */ } /*! + * @param[in] xp Parent xml node. * @param[in] yangi Yang order * @param[in] keynr Length of keyvec/keyval vector when applicable * @param[in] keyvec Array of of yang key identifiers @@ -470,14 +394,10 @@ xml_search_userorder(cxobj *x0, * @param[in] upper Lower bound of childvec search interval */ static cxobj * -xml_search1(cxobj *x0, - char *name, +xml_search1(cxobj *xp, + cxobj *x1, + int userorder, int yangi, - enum rfc_6020 keyword, - int keynr, - char **keyvec, - char **keyval, - cg_var **keycvec, int low, int upper) { @@ -485,61 +405,65 @@ xml_search1(cxobj *x0, int cmp; cxobj *xc; yang_stmt *y; - int userorder= 0; - + if (upper < low) return NULL; /* not found */ mid = (low + upper) / 2; - if (mid >= xml_child_nr(x0)) /* beyond range */ + if (mid >= xml_child_nr(xp)) /* beyond range */ return NULL; - xc = xml_child_i(x0, mid); + xc = xml_child_i(xp, mid); if ((y = xml_spec(xc)) == NULL) return NULL; cmp = yangi-yang_order(y); /* Here is right yang order == same yang? */ if (cmp == 0){ - cmp = xml_cmp1(xc, y, name, keyword, keynr, keyvec, keyval, keycvec, &userorder); - if (userorder && cmp) /* Look inside this yangi order */ - return xml_search_userorder(x0, y, name, yangi, mid, keyword, keynr, keyvec, keyval, keycvec); + if (userorder){ + return xml_search_userorder(xp, x1, y, yangi, mid); + } + else /* Ordered by system */ + cmp = xml_cmp(x1, xc, 0); } if (cmp == 0) return xc; else if (cmp < 0) - return xml_search1(x0, name, yangi, keyword, - keynr, keyvec, keyval, keycvec, low, mid-1); + return xml_search1(xp, x1, userorder, yangi, low, mid-1); else - return xml_search1(x0, name, yangi, keyword, - keynr, keyvec, keyval, keycvec, mid+1, upper); + return xml_search1(xp, x1, userorder, yangi, mid+1, upper); return NULL; } -/*! Find XML children using binary search - * @param[in] yangi yang child order +/*! Find XML child under xp matching x1 using binary search + * @param[in] xp Parent xml node. + * @param[in] yangi Yang child order * @param[in] keynr Length of keyvec/keyval vector when applicable * @param[in] keyvec Array of of yang key identifiers * @param[in] keyval Array of of yang key values */ static cxobj * -xml_search(cxobj *x0, - char *name, - int yangi, - enum rfc_6020 keyword, - int keynr, - char **keyvec, - char **keyval, - cg_var **keycvec) +xml_search(cxobj *xp, + cxobj *x1, + yang_stmt *yc) { - cxobj *xa; - int low = 0; - int upper = xml_child_nr(x0); - + cxobj *xa; + int low = 0; + int upper = xml_child_nr(xp); + int userorder=0; + cxobj *xret = NULL; + int yangi; + /* Assume if there are any attributes, they are first in the list, mask them by raising low to skip them */ for (low=0; lowys_keyword == Y_LIST || yc->ys_keyword == Y_LEAF_LIST) + userorder = (yang_find(yc, Y_ORDERED_BY, "user") != NULL); + yangi = yang_order(yc); + xret = xml_search1(xp, x1, userorder, yangi, low, upper); + return xret; } /*! Insert xn in xp:s sorted child list @@ -604,12 +528,6 @@ xml_insert2(cxobj *xp, */ } if (low +1 == upper){ /* termination criterium */ -#if 0 - if (xml_child_nr(xp) <= mid+1){ - retval = mid; - goto done; - } -#endif if (cmp<0) { retval = mid; goto done; @@ -733,15 +651,8 @@ match_base_child(cxobj *x0, int retval = -1; cvec *cvk = NULL; /* vector of index keys */ cg_var *cvi; - char *b; cxobj *xb; char *keyname; - char keynr = 0; - char **keyval = NULL; - char **keyvec = NULL; - cg_var **keycvec = NULL; - int i; - int yorder; cxobj *x0c = NULL; yang_stmt *y0c; yang_stmt *y0p; @@ -767,19 +678,10 @@ match_base_child(cxobj *x0, case Y_LEAF: /* Equal regardless */ break; case Y_LEAF_LIST: /* Match with name and value */ - keynr = 1; - if ((keyval = calloc(keynr+1, sizeof(char*))) == NULL){ - clicon_err(OE_UNIX, errno, "calloc"); - goto done; - } - if ((keyval[0] = xml_body(x1c)) == NULL) + if (xml_body(x1c) == NULL){ /* Treat as empty string */ + // assert(0); goto ok; - if ((keycvec = calloc(keynr+1, sizeof(cg_var*))) == NULL){ - clicon_err(OE_UNIX, errno, "calloc"); - goto done; } - if (xml_cv_cache(x1c, &keycvec[0]) < 0) /* error case */ - goto done; break; case Y_LIST: /* Match with key values */ cvk = yc->ys_cvec; /* Use Y_LIST cache, see ys_populate_list() */ @@ -787,51 +689,22 @@ match_base_child(cxobj *x0, * Then create two vectors one with names and one with values of x1c, * ec: keyvec: [a,b,c] keyval: [1,2,3] */ - cvi = NULL; keynr = 0; - while ((cvi = cvec_each(cvk, cvi)) != NULL) - keynr++; - if ((keyval = calloc(keynr+1, sizeof(char*))) == NULL){ - clicon_err(OE_UNIX, errno, "calloc"); - goto done; - } - if ((keyvec = calloc(keynr+1, sizeof(char*))) == NULL){ - clicon_err(OE_UNIX, errno, "calloc"); - goto done; - } - if ((keycvec = calloc(keynr+1, sizeof(char*))) == NULL){ - clicon_err(OE_UNIX, errno, "calloc"); - goto done; - } - cvi = NULL; i = 0; + cvi = NULL; while ((cvi = cvec_each(cvk, cvi)) != NULL) { keyname = cv_string_get(cvi); - keyvec[i] = keyname; - if ((xb = xml_find(x1c, keyname)) == NULL) + // keyvec[i] = keyname; + if ((xb = xml_find(x1c, keyname)) == NULL){ goto ok; - if ((b = xml_body(xb)) == NULL) - goto ok; - keyval[i] = b; - if (xml_cv_cache(xb, &keycvec[i]) < 0) /* error case */ - goto done; - i++; + } } - break; default: break; } /* Get match. */ - yorder = yang_order(yc); - x0c = xml_search(x0, xml_name(x1c), yorder, yc->ys_keyword, keynr, keyvec, keyval, keycvec); + x0c = xml_search(x0, x1c, yc); ok: *x0cp = x0c; retval = 0; - done: - if (keyval) - free(keyval); - if (keyvec) - free(keyvec); - if (keycvec) - free(keycvec); return retval; } diff --git a/lib/src/clixon_yang_module.c b/lib/src/clixon_yang_module.c index 4b6c0937..4562d26c 100644 --- a/lib/src/clixon_yang_module.c +++ b/lib/src/clixon_yang_module.c @@ -159,7 +159,8 @@ yang_modules_revision(clicon_handle h) } /*! Actually build the yang modules state XML tree -*/ + * @see RFC7895 + */ static int yms_build(clicon_handle h, yang_stmt *yspec, @@ -197,8 +198,11 @@ yms_build(clicon_handle h, cprintf(cb,"%s", ymod->ys_argument); if ((ys = yang_find(ymod, Y_REVISION, NULL)) != NULL) cprintf(cb,"%s", ys->ys_argument); - else + else{ + /* RFC7895 1 If no (such) revision statement exists, the module's or + submodule's revision is the zero-length string. */ cprintf(cb,""); + } if ((ys = yang_find(ymod, Y_NAMESPACE, NULL)) != NULL) cprintf(cb,"%s", ys->ys_argument); else diff --git a/test/lib.sh b/test/lib.sh index 38a01380..61769b4c 100755 --- a/test/lib.sh +++ b/test/lib.sh @@ -212,7 +212,7 @@ expectfn(){ expect2= fi ret=$($cmd) - r=$? + r=$? # echo "cmd:\"$cmd\"" # echo "retval:\"$retval\"" # echo "expect:\"$expect\"" @@ -221,7 +221,7 @@ expectfn(){ if [ $r != $retval ]; then echo -e "\e[31m\nError ($r != $retval) in Test$testnr [$testname]:" echo -e "\e[0m:" - return + exit -1 fi # if [ $r != 0 ]; then # return diff --git a/test/plot_perf.sh b/test/plot_perf.sh index 80c17c73..5cbc3cc7 100755 --- a/test/plot_perf.sh +++ b/test/plot_perf.sh @@ -1,6 +1,8 @@ #!/bin/bash # Transactions per second for large lists read/write plotter using gnuplot # What do I want to plot? +# First: on i32, i64, arm32 +# PART 1: Basic load # 1. How long to write 100K entries? # - netconf / restconf # - list / leaf-list @@ -9,28 +11,36 @@ # - list / leaf-list # 3. How long to commit 100K entries? (netconf) # - list / leaf-list -# 4. In database 100K entries. How many read operations per second? +# +# PART 2: Load 100K entries. Commit. +# 4. How many read operations per second? # - netconf/ restconf # - list / leaf-list -# 5. 100K entries. How many write operations per second? +# 5. How many write operations per second? # - netconf / restconf # - list / leaf-list -# 6. 100K entries. How may delete operations per second? +# 6. How may delete operations per second? # - netconf / restconf # - list / leaf-list +# The script uses bash builtin "time" command which is somewhat difficult to +# understand. See: https://linux.die.net/man/1/bash # pipelines +# You essentially have to do: { time stuff; } 2>&1 +# See: https://stackoverflow.com/questions/26784870/parsing-the-output-of-bashs-time-builtin # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi -#max=2000 # Nr of db entries -#step=200 -#reqs=500 +# op from step to reqs +to=1000 +step=100 +reqs=100 # Global variables APPNAME=example cfg=$dir/plot-conf.xml fyang=$dir/plot.yang -fconfig=$dir/config +fxml=$dir/data.xml +fjson=$dir/data.json # For memcheck # clixon_netconf="valgrind --leak-check=full --show-leak-kinds=all clixon_netconf" @@ -46,7 +56,7 @@ module scaling{ list y { key "a"; leaf a { - type string; + type uint32; } leaf b { type string; @@ -65,162 +75,257 @@ cat < $cfg $dir /usr/local/share/clixon scaling - /usr/local/var/$APPNAME/$APPNAME.sock - /usr/local/var/example/$APPNAME.pidfile + /usr/local/var/example/example.sock + /usr/local/var/example/example.pidfile false $dir false EOF -# Run function -# args: -# where mode is one of: -# readlist writelist restreadlist restwritelist -runfn(){ - nr=$1 # Number of entries in DB - reqs=$2 - operation=$3 - -# echo "runfn nr=$nr reqs=$reqs mode=$mode" +# Generate file with n entries +# argument: +genfile(){ + if [ $2 = netconf ]; then + echo -n "replace" > $fxml + for (( i=0; i<$1; i++ )); do + echo -n "$i$i" >> $fxml + done + echo "]]>]]>" >> $fxml + else # restconf + echo -n '{"scaling:x":{"y":[' > $fjson + for (( i=0; i<$1; i++ )); do + if [ $i -ne 0 ]; then + echo -n ',' >> $fjson + fi + echo -n "{\"a\":$i,\"b\":\"$i\"}" >> $fjson + done + echo ']}}' >> $fjson + fi +} -# new "generate config with $nr list entries" - echo -n "replace" > $fconfig - for (( i=0; i<$nr; i++ )); do - case $mode in - readlist|writelist|restreadlist|restwritelist) - echo -n "$i$i" >> $fconfig - ;; - writeleaflist) - echo -n "$i" >> $fconfig - ;; - esac - done +# Run netconffunction +# args: +# where proto is one of: +# netconf, restconf +# where op is one of: +# writeall readall commitall read write +runnet(){ + op=$1 + n=$2 # Number of entries in DB + reqs=$3 - echo "]]>]]>" >> $fconfig - -# new "netconf write $nr entry to backend" - expecteof_file "$clixon_netconf -qf $cfg -y $fyang" "$fconfig" "^]]>]]>$" - - case $mode in - readlist) -# new "netconf GET list $reqs" - time -p for (( i=0; i<$reqs; i++ )); do - rnd=$(( ( RANDOM % $nr ) )) - echo "]]>]]>" -done | $clixon_netconf -qf $cfg -y $fyang > /dev/null - ;; - writelist) -# new "netconf WRITE list $reqs" - time -p for (( i=0; i<$reqs; i++ )); do - rnd=$(( ( RANDOM % $nr ) )) - echo "$rnd$rnd]]>]]>" -done | $clixon_netconf -qf $cfg -y $fyang > /dev/null - ;; - restreadlist) -# new "restconf GET list $reqs" - time -p for (( i=0; i<$reqs; i++ )); do - rnd=$(( ( RANDOM % $nr ) )) - curl -sSG http://localhost/restconf/data/scaling:x/y=$rnd > /dev/null -done - ;; - writeleaflist) -# new "netconf GET leaf-list $reqs" - time -p for (( i=0; i<$reqs; i++ )); do - rnd=$(( ( RANDOM % $nr ) )) - echo "$rnd]]>]]>" -done | $clixon_netconf -qf $cfg -y $fyang > /dev/null + echo -n "$n " >> $dir/$op-netconf-$reqs + case $op in + write) + if [ $reqs = 0 ]; then # Write all in one go + genfile $n netconf; + { time -p cat $fxml | $clixon_netconf -qf $cfg -y $fyang ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-netconf-$reqs + else # reqs != 0 + { time -p for (( i=0; i<$reqs; i++ )); do + rnd=$(( ( RANDOM % $n ) )); + echo "$rnd$rnd]]>]]>"; + done | $clixon_netconf -qf $cfg -y $fyang ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-netconf-$reqs + fi ;; + read) + if [ $reqs = 0 ]; then # Read all in one go + { time -p echo "]]>]]>" | $clixon_netconf -qf $cfg -y $fyang > /dev/null ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-netconf-$reqs + else # reqs != 0 + { time -p for (( i=0; i<$reqs; i++ )); do + rnd=$(( ( RANDOM % $nr ) )) + echo "$rnd$rnd]]>]]>" +done | $clixon_netconf -qf $cfg -y $fyang; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-netconf-$reqs + fi + ;; + delete) + { time -p for (( i=0; i<$reqs; i++ )); do + rnd=$(( ( RANDOM % $nr ) )) + echo "$rnd$rnd]]>]]>" +done | $clixon_netconf -qf $cfg -y $fyang; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-netconf-$reqs + ;; + commit) + { time -p echo "]]>]]>" | $clixon_netconf -qf $cfg -y $fyang > /dev/null ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-netconf-$reqs + ;; + *) + err "Operation not supported" "$op" + exit + ;; esac -# new "discard test" - expecteof "$clixon_netconf -qf $cfg -y $fyang" "]]>]]>" "^]]>]]>$" - } -# Step -# args: -stepfn(){ - i=$1 - reqs=$2 - mode=$3 - ->&2 echo "stepfn $mode: i=$i reqs=$reqs" - echo -n "" > $fconfig - t=$(TEST=%e runfn $i $reqs $mode 2>&1 | awk '/real/ {print $2}') - #TEST=%e runfn $i $reqs $mode 2>&1 - # t is time in secs of $reqs -> transactions per second. $reqs - p=$(echo "$reqs/$t" | bc -lq) - # p is transactions per second. - # write to gnuplot file: $dir/$mode - echo "$i $p" >> $dir/$mode +# Run restconf function +# args: +# where proto is one of: +# netconf, restconf +# where op is one of: +# writeall readall commitall read write +runrest(){ + op=$1 + n=$2 # Number of entries in DB + reqs=$3 + + echo -n "$n " >> $dir/$op-restconf-$reqs + case $op in + write) + if [ $reqs = 0 ]; then # Write all in one go + genfile $n restconf + # restconf @- means from stdin + { time -p curl -sS -X PUT -d @$fjson http://localhost/restconf/data/scaling:x ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-restconf-$reqs + else # Small requests + { time -p for (( i=0; i<$reqs; i++ )); do + rnd=$(( ( RANDOM % $n ) )); + curl -sS -X PUT http://localhost/restconf/data/scaling:x/y=$rnd -d "{\"scaling:y\":{\"a\":$rnd,\"b\":\"$rnd\"}}" + done ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-restconf-$reqs + # + fi + ;; + read) + if [ $reqs = 0 ]; then # Read all in one go + { time -p curl -sS -X GET http://localhost/restconf/data/scaling:x > /dev/null; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-restconf-$reqs + else # Small requests + { time -p for (( i=0; i<$reqs; i++ )); do + rnd=$(( ( RANDOM % $n ) )); + curl -sS -X GET http://localhost/restconf/data/scaling:x/y=$rnd + done ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-restconf-$reqs + fi + ;; + delete) + { time -p for (( i=0; i<$reqs; i++ )); do + rnd=$(( ( RANDOM % $n ) )); + curl -sS -X GET http://localhost/restconf/data/scaling:x/y=$rnd + done ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-restconf-$reqs + + ;; + *) + err "Operation not supported" "$op" + exit + ;; + esac } -# Run once -#args: -once(){ - # Input Parameters - step=$1 - reqs=$2 - max=$3 - echo "oncefn step=$step reqs=$reqs max=$max" - new "test params: -f $cfg -y $fyang" - if [ $BE -ne 0 ]; then - new "kill old backend" - sudo clixon_backend -zf $cfg -y $fyang - if [ $? -ne 0 ]; then - err - fi - new "start backend -s init -f $cfg -y $fyang" - start_backend -s init -f $cfg -y $fyang +commit(){ + # commit to running + expecteof "$clixon_netconf -qf $cfg -y $fyang" 0 "]]>]]>" "^]]>]]>$" +} + +reset(){ + # delete all in candidate + expecteof "$clixon_netconf -qf $cfg -y $fyang" 0 "none]]>]]>" '^]]>]]>$' + # commit to running + commit +} + +# Load n entries into candidate +# Args: +load(){ + # Generate file ($fxml) + genfile $1 netconf + # Write it to backend in one chunk + expecteof_file "$clixon_netconf -qf $cfg -y $fyang" "$fxml" "^]]>]]>$" +} + +# Run an operation, iterate from to in increment of +# Each operation do times +# args: +# =0 means all in one go +# means a priori loaded into datastore +plot(){ + op=$1 + proto=$2 + from=$3 + step=$4 + to=$5 + reqs=$6 + can=$7 + run=$8 + + if [ $# -ne 8 ]; then + exit "plot should be called with 8 arguments, got $#" fi - - new "kill old restconf daemon" - sudo pkill -u www-data -f "/www-data/clixon_restconf" - - new "start restconf daemon" - start_restconf -f $cfg -y $fyang - - new "waiting" - sleep $RCWAIT - - new "Intial steps as start" - for (( i=10; i<=$step; i=i+10 )); do - stepfn $i $reqs readlist - stepfn $i $reqs writelist - stepfn $i $reqs restreadlist - stepfn $i $reqs writeleaflist - done - rnd=$(( ( RANDOM % $step ) )) - echo "curl -sSG http://localhost/restconf/data/scaling:x/y=$rnd" - curl -sSG http://localhost/restconf/data/scaling:x/y=$rnd -exit - new "Actual steps" - for (( i=$step; i<=$max; i=i+$step )); do - stepfn $i $reqs readlist - stepfn $i $reqs writelist - stepfn $i $reqs restreadlist - stepfn $i $reqs writeleaflist - done - - new "Kill restconf daemon" - stop_restconf - - if [ $BE -ne 0 ]; then - new "Kill backend" - # Check if premature kill - pid=`pgrep -u root -f clixon_backend` - if [ -z "$pid" ]; then - err "backend already dead" + + # reset file + new "Create file $dir/$op-$proto-$reqs" + echo "" > $dir/$op-$proto-$reqs + for (( n=$from; n<=$to; n=$n+$step )); do + reset + if [ $can = n ]; then + load $n + if [ $run = n ]; then + commit + fi fi - # kill backend - stop_backend -f $cfg - fi + new "$op-$proto-$reqs $n" + if [ $proto = netconf ]; then + runnet $op $n $reqs + else + runrest $op $n $reqs + fi + done + echo # newline } -# step=200 reqs=500 max=2000 -once 200 500 1000 +new "test params: -f $cfg -y $fyang" +if [ $BE -ne 0 ]; then + new "kill old backend" + sudo clixon_backend -zf $cfg -y $fyang + if [ $? -ne 0 ]; then + err + fi + new "start backend -s init -f $cfg -y $fyang" + start_backend -s init -f $cfg -y $fyang +fi +new "kill old restconf daemon" +sudo pkill -u www-data -f "/www-data/clixon_restconf" + +new "start restconf daemon" +start_restconf -f $cfg -y $fyang + +new "waiting" +sleep $RCWAIT + +for proto in netconf restconf; do + new "$proto write all entries to candidate (restconf:running)" + plot write $proto $step $step $to 0 0 0 # all candidate 0 running 0 +done + +for proto in netconf restconf; do + new "$proto read all entries from running" + plot read netconf $step $step $to 0 n n # start w full datastore +done + +new "Netconf commit all entries from candidate to running" +plot commit netconf $step $step $to 0 n 0 # candidate full running empty + +reqs=100 +for proto in netconf restconf; do + new "$proto read $reqs from full database" + plot read $proto $step $step $to $reqs n n + + new "$proto Write $reqs to full database(replace / alter values)" + plot write $proto $step $step $to $reqs n n + + new "$proto delete $reqs from full database(replace / alter values)" + plot delete $proto $step $step $to $reqs n n +done + +new "Kill restconf daemon" +stop_restconf + +if [ $BE -ne 0 ]; then + new "Kill backend" + # Check if premature kill + pid=`pgrep -u root -f clixon_backend` + if [ -z "$pid" ]; then + err "backend already dead" + fi + # kill backend + stop_backend -f $cfg +fi + +arch=$(arch) gnuplot -persist < +expecteq "$(curl -s -X GET http://localhost/.well-known/host-meta)" 0 " " diff --git a/test/test_restconf2.sh b/test/test_restconf2.sh index 543a9799..0ec36b11 100755 --- a/test/test_restconf2.sh +++ b/test/test_restconf2.sh @@ -95,7 +95,7 @@ expectfn 'curl -s -X POST -d {"example:cont1":{"interface":{"type":"regular"}}} new "restconf POST initial tree" expectfn 'curl -s -X POST -d {"example:cont1":{"interface":{"name":"local0","type":"regular"}}} http://localhost/restconf/data' 0 "" -new "restconf GET datastore intial" +new "restconf GET datastore initial" expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1": {"interface": \[{"name": "local0","type": "regular"}\]}}' new "restconf GET interface subtree" From 14d319dd9ba05dcdff0e450e777a8d0432beff5c Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Wed, 17 Apr 2019 11:26:05 +0200 Subject: [PATCH 03/13] * A new "hello world" example is added * The directory `docker/system` has been moved to `docker/main` --- .gitignore | 1 + CHANGELOG.md | 2 + configure | 5 +- configure.ac | 5 +- doc/FAQ.md | 5 + docker/Makefile.in | 2 +- docker/README.md | 4 +- docker/base/README.md | 4 +- docker/{system => main}/Dockerfile | 0 docker/{system => main}/Makefile.in | 0 docker/{system => main}/README.md | 0 docker/{system => main}/cleanup.sh | 0 docker/{system => main}/start.sh | 0 docker/{system => main}/startsystem.sh | 0 example/README.md | 4 +- example/hello/Makefile.in | 167 +++++++++++++++++++++ example/hello/README.md | 111 ++++++++++++++ example/hello/clixon-hello@2019-04-17.yang | 14 ++ example/hello/hello.xml | 13 ++ example/hello/hello_cli.cli | 54 +++++++ example/main/README.md | 2 +- 21 files changed, 382 insertions(+), 11 deletions(-) rename docker/{system => main}/Dockerfile (100%) rename docker/{system => main}/Makefile.in (100%) rename docker/{system => main}/README.md (100%) rename docker/{system => main}/cleanup.sh (100%) rename docker/{system => main}/start.sh (100%) rename docker/{system => main}/startsystem.sh (100%) create mode 100644 example/hello/Makefile.in create mode 100644 example/hello/README.md create mode 100644 example/hello/clixon-hello@2019-04-17.yang create mode 100644 example/hello/hello.xml create mode 100644 example/hello/hello_cli.cli diff --git a/.gitignore b/.gitignore index c191c5b0..4927378c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ docker/Makefile docker/*/Makefile etc/Makefile example/Makefile +example/*/Makefile lib/Makefile lib/*/Makefile test/Makefile diff --git a/CHANGELOG.md b/CHANGELOG.md index e5b54800..7edfe80e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ ### API changes on existing features (you may need to change your code) +* The directory `docker/system` has been moved to `docker/main`, to reflect that it runs the main example. * xmldb_get() removed "config" parameter: * Change all calls to dbget from: `xmldb_get(h, db, xpath, 0|1, &xret, msd)` to `xmldb_get(h, db, xpath, &xret, msd)` * Structural change: removed datastore plugin and directory, and merged into regular clixon lib code. @@ -101,6 +102,7 @@ ### Minor changes +* A new "hello world" example is added * Optimized validation of large lists * New xmldb_get1() returning actual cache - not a copy. This has lead to some householding instead of just deleting the copy * xml_diff rewritten to work linearly instead of O(2) diff --git a/configure b/configure index 64161685..bc0eed46 100755 --- a/configure +++ b/configure @@ -4447,7 +4447,7 @@ _ACEOF -ac_config_files="$ac_config_files Makefile lib/Makefile lib/src/Makefile lib/clixon/Makefile apps/Makefile apps/cli/Makefile apps/backend/Makefile apps/netconf/Makefile apps/restconf/Makefile include/Makefile etc/Makefile etc/clixonrc example/Makefile example/main/Makefile extras/rpm/Makefile docker/Makefile docker/system/Makefile docker/base/Makefile util/Makefile yang/Makefile yang/clixon/Makefile yang/standard/Makefile doc/Makefile test/Makefile" +ac_config_files="$ac_config_files Makefile lib/Makefile lib/src/Makefile lib/clixon/Makefile apps/Makefile apps/cli/Makefile apps/backend/Makefile apps/netconf/Makefile apps/restconf/Makefile include/Makefile etc/Makefile etc/clixonrc example/Makefile example/main/Makefile example/hello/Makefile extras/rpm/Makefile docker/Makefile docker/main/Makefile docker/base/Makefile util/Makefile yang/Makefile yang/clixon/Makefile yang/standard/Makefile doc/Makefile test/Makefile" cat >confcache <<\_ACEOF # This file is a shell script that caches the results of configure @@ -5155,9 +5155,10 @@ do "etc/clixonrc") CONFIG_FILES="$CONFIG_FILES etc/clixonrc" ;; "example/Makefile") CONFIG_FILES="$CONFIG_FILES example/Makefile" ;; "example/main/Makefile") CONFIG_FILES="$CONFIG_FILES example/main/Makefile" ;; + "example/hello/Makefile") CONFIG_FILES="$CONFIG_FILES example/hello/Makefile" ;; "extras/rpm/Makefile") CONFIG_FILES="$CONFIG_FILES extras/rpm/Makefile" ;; "docker/Makefile") CONFIG_FILES="$CONFIG_FILES docker/Makefile" ;; - "docker/system/Makefile") CONFIG_FILES="$CONFIG_FILES docker/system/Makefile" ;; + "docker/main/Makefile") CONFIG_FILES="$CONFIG_FILES docker/main/Makefile" ;; "docker/base/Makefile") CONFIG_FILES="$CONFIG_FILES docker/base/Makefile" ;; "util/Makefile") CONFIG_FILES="$CONFIG_FILES util/Makefile" ;; "yang/Makefile") CONFIG_FILES="$CONFIG_FILES yang/Makefile" ;; diff --git a/configure.ac b/configure.ac index d7e2e741..47b79aa2 100644 --- a/configure.ac +++ b/configure.ac @@ -248,10 +248,11 @@ AC_OUTPUT(Makefile etc/Makefile etc/clixonrc example/Makefile - example/main/Makefile + example/main/Makefile + example/hello/Makefile extras/rpm/Makefile docker/Makefile - docker/system/Makefile + docker/main/Makefile docker/base/Makefile util/Makefile yang/Makefile diff --git a/doc/FAQ.md b/doc/FAQ.md index 0afffdc0..a5c81437 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -6,6 +6,7 @@ * [Is Clixon extendible?](#is-clixon-extendible) * [Which programming language is used?](#which-programming-language-is-used) * [How to best understand Clixon?](#how-to-best-understand-clixon) + * [Hello world?](#hello-world) * [How do you build and install Clixon (and the example)?](how-do-you-build-and-install-clixon) * [How do I run Clixon example commands?](#how-do-i-run-clixon-example-commands) * [Do I need to setup anything? (IMPORTANT)](#do-i-need-to-setup-anything)) @@ -67,6 +68,10 @@ specification uses [CLIgen](http://github.com/olofhagsand/cligen) ## How to best understand Clixon? Run the Clixon example, in the [example](../example) directory. +## Hello world? + +One of the examples is [a hello world example](../example/hello). Please start with that. + ## How do you build and install Clixon? Clixon: ``` diff --git a/docker/Makefile.in b/docker/Makefile.in index 51443a4e..83bb7191 100644 --- a/docker/Makefile.in +++ b/docker/Makefile.in @@ -41,7 +41,7 @@ LIBS = @LIBS@ SHELL = /bin/sh SUBDIRS = base -SUBDIRS += system +SUBDIRS += main #SUBDIRS += cluster .PHONY: all clean distclean depend install-include install uninstall test $(SUBDIRS) diff --git a/docker/README.md b/docker/README.md index ad190774..117705ee 100644 --- a/docker/README.md +++ b/docker/README.md @@ -2,6 +2,6 @@ This directory contains sub-directories with examples of Clixon docker images: - * [base](base/README.md) Clixon base image - * [system](system/README.md) Example and test application + * [base](base/README.md) Clixon base image + * [main](main/README.md) Main example and test application diff --git a/docker/base/README.md b/docker/base/README.md index de26e094..0acdca79 100644 --- a/docker/base/README.md +++ b/docker/base/README.md @@ -8,7 +8,7 @@ The clixon docker base image can be used to build clixon applications. It has all the whole code for a clixon release which it downloads from git. -See [clixon-system](../system/README.md) for a more complete clixon image. +See [clixon-system](../main/README.md) for a more complete clixon image. ## Build and push @@ -20,7 +20,7 @@ You may also do `make push` if you want to push the image, but you may then cons ## Example run -The base container is a minimal and primitive example. Look at the [clixon-system](../system) for a more stream-lined application. +The base container is a minimal and primitive example. Look at the [clixon-system](../main) for a more stream-lined application. The following shows a simple example of how to run the example application. First, the container is started with the backend running: diff --git a/docker/system/Dockerfile b/docker/main/Dockerfile similarity index 100% rename from docker/system/Dockerfile rename to docker/main/Dockerfile diff --git a/docker/system/Makefile.in b/docker/main/Makefile.in similarity index 100% rename from docker/system/Makefile.in rename to docker/main/Makefile.in diff --git a/docker/system/README.md b/docker/main/README.md similarity index 100% rename from docker/system/README.md rename to docker/main/README.md diff --git a/docker/system/cleanup.sh b/docker/main/cleanup.sh similarity index 100% rename from docker/system/cleanup.sh rename to docker/main/cleanup.sh diff --git a/docker/system/start.sh b/docker/main/start.sh similarity index 100% rename from docker/system/start.sh rename to docker/main/start.sh diff --git a/docker/system/startsystem.sh b/docker/main/startsystem.sh similarity index 100% rename from docker/system/startsystem.sh rename to docker/main/startsystem.sh diff --git a/example/README.md b/example/README.md index 709cf2dd..58cdc401 100644 --- a/example/README.md +++ b/example/README.md @@ -1,4 +1,6 @@ # Clixon examples Clixon have the following examples: - * [Main example](main/README.md) \ No newline at end of file + * [Main example](main/README.md) + * [Hello world](hello/README.md) + \ No newline at end of file diff --git a/example/hello/Makefile.in b/example/hello/Makefile.in new file mode 100644 index 00000000..7ba400aa --- /dev/null +++ b/example/hello/Makefile.in @@ -0,0 +1,167 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# +# Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren +# +# This file is part of CLIXON +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Alternatively, the contents of this file may be used under the terms of +# the GNU General Public License Version 3 or later (the "GPL"), +# in which case the provisions of the GPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of the GPL, and not to allow others to +# use your version of this file under the terms of Apache License version 2, +# indicate your decision by deleting the provisions above and replace them with +# the notice and other provisions required by the GPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the Apache License version 2 or the GPL. +# +# ***** END LICENSE BLOCK ***** +# +VPATH = @srcdir@ +srcdir = @srcdir@ +top_srcdir = @top_srcdir@ +prefix = @prefix@ +bindir = @bindir@ +includedir = @includedir@ +datarootdir = @datarootdir@ +sysconfdir = @sysconfdir@ +datarootdir = @datarootdir@ +localstatedir = @localstatedir@ +libdir = @exec_prefix@/lib + +APPNAME = hello + +# Here is where example yang appears +CLIXON_DATADIR = @CLIXON_DATADIR@ +# Install here if you want default clixon location: +CLIXON_DEFAULT_CONFIG = @CLIXON_DEFAULT_CONFIG@ + +CC = @CC@ +CFLAGS = @CFLAGS@ -rdynamic -fPIC +INSTALLFLAGS = @INSTALLFLAGS@ +with_restconf = @with_restconf@ + +INCLUDES = -I$(includedir) @INCLUDES@ +CPPFLAGS = @CPPFLAGS@ -fPIC + +BE_PLUGIN = $(APPNAME)_backend.so +CLI_PLUGIN = $(APPNAME)_cli.so +NETCONF_PLUGIN = $(APPNAME)_netconf.so +RESTCONF_PLUGIN = $(APPNAME)_restconf.so + +PLUGINS = $(BE_PLUGIN) $(CLI_PLUGIN) $(NETCONF_PLUGIN) +ifeq ($(with_restconf),yes) +PLUGINS += $(RESTCONF_PLUGIN) +endif + +.PHONY: all clean depend install + +all: $(PLUGINS) + +.SUFFIXES: .c .o + +# implicit rule +.c.o: + $(CC) $(INCLUDES) $(CPPFLAGS) $(CFLAGS) -c $< + +CLISPECS = $(APPNAME)_cli.cli + +YANGSPECS = clixon-hello@2019-04-17.yang + +# CLI frontend plugin (see also install rule) +#CLI_SRC = $(APPNAME)_cli.c +CLI_OBJ = $(CLI_SRC:%.c=%.o) +$(CLI_PLUGIN): $(CLI_OBJ) + $(CC) -Wall -shared -o $@ -lc $^ + +# Backend plugin (see also install rule) +#BE_SRC = $(APPNAME)_backend.c +BE_OBJ = $(BE_SRC:%.c=%.o) +$(BE_PLUGIN): $(BE_OBJ) + $(CC) -Wall -shared -o $@ -lc $< + +# NETCONF frontend plugin (see also install rule) +#NETCONF_SRC = $(APPNAME)_netconf.c +NETCONF_OBJ = $(NETCONF_SRC:%.c=%.o) +$(NETCONF_PLUGIN): $(NETCONF_OBJ) + $(CC) -Wall -shared -o $@ -lc $^ + +# See configure.ac for disabling restconf +# RESTCONF frontend plugin (see also install rule) +#RESTCONF_SRC = $(APPNAME)_restconf.c +RESTCONF_OBJ = $(RESTCONF_SRC:%.c=%.o) +$(RESTCONF_PLUGIN): $(RESTCONF_OBJ) + $(CC) -Wall -shared -o $@ -lc $^ + +SRC = $(BE_SRC) $(CLI_SRC) $(NETCONF_SRC) +SRC += $(RESTCONF_SRC) + +OBJS = $(BE_OBJ) $(CLI_OBJ) $(NETCONF_OBJ) +OBJS += $(RESTCONF_OBJ) + +clean: + rm -f $(PLUGINS) $(OBJS) + +distclean: clean + rm -f Makefile *~ .depend + + +install: $(YANGSPECS) $(CLISPECS) $(PLUGINS) $(APPNAME).xml + install -m 0644 $(APPNAME).xml $(DESTDIR)$(CLIXON_DEFAULT_CONFIG) + install -d -m 0755 $(DESTDIR)$(libdir)/$(APPNAME) + install -d -m 0755 $(DESTDIR)$(libdir)/$(APPNAME)/clispec + install -m 0644 $(CLISPECS) $(DESTDIR)$(libdir)/$(APPNAME)/clispec + install -d -m 0755 $(DESTDIR)$(datarootdir)/$(APPNAME)/yang + install -m 0644 $(YANGSPECS) $(DESTDIR)$(DESTDIR)$(CLIXON_DATADIR) + install -d -m 0755 $(DESTDIR)$(localstatedir)/$(APPNAME) + +# Uncomment for installing config file in /usr/local/etc instead +# install -d -m 0755 $(DESTDIR)$(sysconfdir) +# install -m 0644 $(APPNAME).xml $(DESTDIR)$(sysconfdir) + +# Uncomment for installing cli plugin (see CLI_SRC) +# install -d -m 0755 $(DESTDIR)$(libdir)/$(APPNAME)/cli +# install -m 0644 $(INSTALLFLAGS) $(CLI_PLUGIN) $(DESTDIR)$(libdir)/$(APPNAME)/cli + +# Uncomment for installing backend plugin (see BE_SRC) +# install -d -m 0755 $(DESTDIR)$(libdir)/$(APPNAME)/backend +# install -m 0644 $(INSTALLFLAGS) $(BE_PLUGIN) $(DESTDIR)$(libdir)/$(APPNAME)/backend + +# Uncomment for installing netconf plugin (see NETCONF_SRC) +# install -d -m 0755 $(DESTDIR)$(libdir)/$(APPNAME)/netconf +# install -m 0644 $(INSTALLFLAGS) $(NETCONF_PLUGIN) $(DESTDIR)$(libdir)/$(APPNAME)/netconf + +# Uncomment for installing restconf plugin (see RESTCONF_SRC). The conditional is +# because it is an option to disable restconf +#ifeq ($(with_restconf),yes) +# install -d -m 0755 $(DESTDIR)$(libdir)/$(APPNAME)/restconf +# install -m 0644 $(INSTALLFLAGS) $(RESTCONF_PLUGIN) $(DESTDIR)$(libdir)/$(APPNAME)/restconf +#endif + +uninstall: + rm -rf $(DESTDIR)$(CLIXON_DEFAULT_CONFIG) + rm -rf $(DESTDIR)$(libdir)/$(APPNAME) + rm -rf $(DESTDIR)$(localstatedir)/$(APPNAME) + rm -rf $(DESTDIR)$(datarootdir)/$(APPNAME) + + +install-include: + +depend: + $(CC) $(DEPENDFLAGS) $(INCLUDES) $(CFLAGS) -MM $(SRC) > .depend + +#include .depend + diff --git a/example/hello/README.md b/example/hello/README.md new file mode 100644 index 00000000..0c7c3a0b --- /dev/null +++ b/example/hello/README.md @@ -0,0 +1,111 @@ +# Clixon hello world example + + * [Content](#content) + * [Compile and run](#compile) + * [Using the CLI](#using-the-cli) + * [Netconf](#netconf) + * [Restconf](#restconf) + * [Next steps](#next-steps) + +## Content + +This directory contains a Clixon example which includes a simple example. It contains the following files: +* `hello.xml` The configuration file. See [yang/clixon-config@.yang](../../yang/clixon-config@2019-03-05.yang) for the documentation of all available fields. +* `clixon-hello@2019-04-17.yang` The yang spec of the example. +* `hello_cli.cli` CLIgen specification. +* `Makefile.in` Example makefile where plugins are built and installed +* `README.md` This file + + +## Compile and run + +Before you start, +* Make [group setup](../../doc/FAQ.md#do-i-need-to-setup-anything-important) + +``` + make && sudo make install +``` +Start backend in the background: +``` + sudo clixon_backend +``` +Start cli: +``` + clixon_cli +``` + +## Using the CLI + +The example CLI allows you to modify and view the data model using `set`, `delete` and `show` via generated code. + +The following example shows how to add a very simple configuration `hello world` using the generated CLI. The config is added to the candidate database, shown, committed to running, and then deleted. +``` + olof@vandal> clixon_cli + cli> set + hello + cli> set hello world + cli> show configuration + hello world; + cli> commit + cli> delete + all Delete whole candidate configuration + hello + cli> delete hello + cli> show configuration + cli> commit + cli> quit + olof@vandal> +``` + +## Netconf + +Clixon also provides a Netconf interface. The following example starts a netconf client form the shell, adds the hello world config, commits it, and shows it: +``` + olof@vandal> clixon_netconf -q + ]]>]]> + ]]>]]> + ]]>]]> + ]]>]]> + ]]>]]> + ]]>]]> +olof@vandal> +``` + +## Restconf + +Clixon also provides a Restconf interface. A reverse proxy needs to be configured. There are [instructions how to setup Nginx](../../doc/FAQ.md#how-do-i-use-restconf) for Clixon. + +Start restconf daemon +``` + sudo su -c "/www-data/clixon_restconf" -s /bin/sh www-data & +``` + +Start sending restconf commands (using Curl): +``` + olof@vandal> curl -X POST http://localhost/restconf/data -d '{"clixon-hello:hello":{"world":null}}' + olof@vandal> curl -X GET http://localhost/restconf/data + { + "data": { + "clixon-hello:hello": { + "world": null + } + } + } +``` + +## Next steps + +The hello world example only has a Yang spec and a template CLI +spec. For more advanced applications, customized backend, cli, netconf +and restconf code callbacks becomes necessary. + +Further, you may want to add upgrade, RPC:s, state data, notification +streams, authentication and authorization. The [main example](../main) +contains examples for such capabilities. + +There are also [container examples](../../docker) and lots more. + + + + + diff --git a/example/hello/clixon-hello@2019-04-17.yang b/example/hello/clixon-hello@2019-04-17.yang new file mode 100644 index 00000000..89c30acd --- /dev/null +++ b/example/hello/clixon-hello@2019-04-17.yang @@ -0,0 +1,14 @@ +module clixon-hello { + yang-version 1.1; + namespace "urn:example:hello"; + prefix he; + revision 2019-04-17 { + description + "Clixon hello world example"; + } + container hello{ + container world{ + presence true; + } + } +} diff --git a/example/hello/hello.xml b/example/hello/hello.xml new file mode 100644 index 00000000..1210e1f2 --- /dev/null +++ b/example/hello/hello.xml @@ -0,0 +1,13 @@ + + /usr/local/etc/example.xml + *:* + /usr/local/share/clixon + clixon-hello + hello + /usr/local/lib/hello/clispec + /usr/local/var/hello.sock + /usr/local/var/hello.pidfile + /usr/local/var/hello + init + false + diff --git a/example/hello/hello_cli.cli b/example/hello/hello_cli.cli new file mode 100644 index 00000000..a3b469f5 --- /dev/null +++ b/example/hello/hello_cli.cli @@ -0,0 +1,54 @@ +# Common CLI syntax for both server and PMNode operatio mode +CLICON_MODE="hello"; +CLICON_PROMPT="cli> "; + +# Reference generated data model +set @datamodel, cli_set(); +merge @datamodel, cli_merge(); +create @datamodel, cli_create(); +delete("Delete a configuration item") @datamodel, cli_del(); + +validate("Validate changes"), cli_validate(); +commit("Commit the changes"), cli_commit(); +quit("Quit"), cli_quit(); +delete("Delete a configuration item") all("Delete whole candidate configuration"), delete_all("candidate"); + +startup("Store running as startup config"), db_copy("running", "startup"); +no("Negate or remove") debug("Debugging parts of the system"), cli_debug_cli((int32)0); +debug("Debugging parts of the system"), cli_debug_cli((int32)1);{ + level("Set debug level: 1..n") ("Set debug level (0..n)"), cli_debug_backend(); +} +discard("Discard edits (rollback 0)"), discard_changes(); +compare("Compare running and candidate"), compare_dbs((int32)1); + +show("Show a particular state of the system"){ + xpath("Show configuration") ("XPATH expression"), show_conf_xpath("candidate"); + version("Show version"), cli_show_version("candidate", "text", "/"); + compare("Compare candidate and running databases"), compare_dbs((int32)0);{ + xml("Show comparison in xml"), compare_dbs((int32)0); + text("Show comparison in text"), compare_dbs((int32)1); + } + configuration("Show configuration"), cli_show_config("candidate", "text", "/");{ + xml("Show configuration as XML"), cli_show_config("candidate", "xml", "/");{ + @datamodel, cli_show_auto("candidate", "xml"); + } + cli("Show configuration as CLI commands"), cli_show_config("candidate", "cli", "/");{ + @datamodel, cli_show_auto("candidate", "cli"); + } + netconf("Show configuration as netconf edit-config operation"), cli_show_config("candidate", "netconf", "/");{ + @datamodel, cli_show_auto("candidate", "netconf"); + } + text("Show configuration as text"), cli_show_config("candidate","text","/");{ + @datamodel, cli_show_auto("candidate", "text"); + } + json("Show configuration as JSON"), cli_show_config("candidate", "json", "/");{ + @datamodel, cli_show_auto("candidate", "json"); + } + } +} + +save("Save candidate configuration to XML file") ("Filename (local filename)"), save_config_file("candidate","filename"); +load("Load configuration from XML file") ("Filename (local filename)"),load_config_file("filename", "replace");{ + replace("Replace candidate with file contents"), load_config_file("filename", "replace"); + merge("Merge file with existent candidate"), load_config_file("filename", "merge"); +} diff --git a/example/main/README.md b/example/main/README.md index 76f9eb02..16215237 100644 --- a/example/main/README.md +++ b/example/main/README.md @@ -15,7 +15,7 @@ ## Content This directory contains a Clixon example which includes a simple example. It contains the following files: -* `example.xml` The configuration file. See [yang/clixon-config@.yang](../../yang/clixon-config@2018-10-21.yang) for the documentation of all available fields. +* `example.xml` The configuration file. See [yang/clixon-config@.yang](../../yang/clixon-config@2019-03-05.yang) for the documentation of all available fields. * `clixon-example@2019-01-13.yang` The yang spec of the example. * `example_cli.cli` CLIgen specification. * `example_cli.c` CLI callback plugin containing functions called in the cli file above: a generic callback (`mycallback`) and an example RPC call (`example_client_rpc`). From c3d9b392ddc771673bb1ede7b61dfb4205ebbbf2 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Wed, 17 Apr 2019 15:50:38 +0200 Subject: [PATCH 04/13] Scaling large lists report --- CHANGELOG.md | 1 + README.md | 3 +- doc/large-lists.md | 134 ------------- doc/scaling/clixon-commit-0.png | Bin 0 -> 6810 bytes doc/scaling/clixon-delete-100.png | Bin 0 -> 8666 bytes doc/scaling/clixon-get-0.png | Bin 0 -> 8200 bytes doc/scaling/clixon-get-100.png | Bin 0 -> 8588 bytes doc/scaling/clixon-put-0.png | Bin 0 -> 7917 bytes doc/scaling/clixon-put-100.png | Bin 0 -> 8277 bytes doc/scaling/large-lists.md | 131 +++++++++++++ example/README.md | 3 +- test/plot_perf.sh | 312 +++++++++++++++++++++--------- 12 files changed, 354 insertions(+), 230 deletions(-) delete mode 100644 doc/large-lists.md create mode 100644 doc/scaling/clixon-commit-0.png create mode 100644 doc/scaling/clixon-delete-100.png create mode 100644 doc/scaling/clixon-get-0.png create mode 100644 doc/scaling/clixon-get-100.png create mode 100644 doc/scaling/clixon-put-0.png create mode 100644 doc/scaling/clixon-put-100.png create mode 100644 doc/scaling/large-lists.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 7edfe80e..57215d1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,7 @@ ### Minor changes +* A scaling of [large lists](doc/scaling) report is added * A new "hello world" example is added * Optimized validation of large lists * New xmldb_get1() returning actual cache - not a copy. This has lead to some householding instead of just deleting the copy diff --git a/README.md b/README.md index 38385af8..b26a32d2 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ support. * [Background](#background) * [Frequently asked questions (FAQ)](doc/FAQ.md) + * [Hello world](example/hello/README.md) * [Changelog](CHANGELOG.md) * [Installation](#installation) * [Licenses](#licenses) @@ -26,7 +27,7 @@ support. * [Runtime](#runtime) * [Clixon project page](http://www.clicon.org) * [Tests and CI](test/README.md) - * [Scaling: large lists](doc/large-lists.md) + * [Scaling: large lists](doc/scaling/large-lists.md) * [Containers](docker/README.md) * [Roadmap](doc/ROADMAP.md) * [Reference manual](#reference) diff --git a/doc/large-lists.md b/doc/large-lists.md deleted file mode 100644 index 1b1b15a4..00000000 --- a/doc/large-lists.md +++ /dev/null @@ -1,134 +0,0 @@ -# Large lists in Clixon - - * [Background](#background) - * [Overview](#overview) - * [Test descriptions]#test-descriptions) - -## Background - -Clixon is a configuration management tool. In this paper the case of -a large number of "flat" list and leaf-list entries are investigated. -There may be other scaling usecases, such as large configuratin -"depth", large number of requesting clients, etc. However, these are -not investigated here. - -## Overview -The basic case is a large list, according to the following Yang specification: -``` - list y { - key "a"; - leaf a { - type int32; - } - leaf b { - type string; - } - } -``` -where `a` is a unique key and `b` is a payload, useful in replace operations. - -There is also a leaf-list as follows: -``` - leaf-list c { - type string; - } -``` - -XML lists with `N` elements are generated based on -this configuration, eg for `N=10`: -``` - 00 - 11 - 22 - 33 - 44 - 55 - 66 - 77 - 88 - 99 -``` - -Requests are made using a random function, a request on the list above will on the form: -``` - curl -G http://localhost/restconf/data/y=(rnd%$N) -``` - -## Test descriptions - -### Limitations - -Test were not made using CLI interaction. - -### Setup - -The setup consisted of the following components running on the same machine: -* A clixon backend daemon -* A clixon restconf daemon -* An nginx daemon daemon -* A netconf client program -* curl client -* A bash terminal and test script [plot_perf.sh](../test/plot_perf.sh) -* Gnuplot for generating plots - -### Config file -The following Clixon config file was used: -``` - - $cfg - $dir - /usr/local/share/clixon - scaling - /usr/local/var/example/example.sock - /usr/local/var/example/example.pidfile - false - $dir - false - -``` -where `$dir` and `$cfg`are local files. For more info see [plot_perf.sh]. - -### Testcases - -All tests measure the "real" time of a command on a lightly loaded -machine using the Linux command `time(1)`. - -The following tests were made (for each architecture and protocol): -* Write `N` entries in one single operation. (With an empty datastore) -* Read `N` entries in one single operation. (With a datastore of `N` entries) -* Commit `N` entries (With a candidate of `N` entries and empty running) -* Read 1 entry (In a datastore of `N` entries) -* Write/Replace 1 entry (In a datastore of `N` entries) -* Delete 1 entry (In a datastore of `N` entries) - -### Protocols - -The tests are made using: -* Netconf[RFC6241] and -* Restconf[RFC8040]. -Notably, CLI tests are for future study. - -### Architectures - -The tests were made on the following hardware, all running Ubuntu Linux: -* [i686] dual Intel Core Duo processor (IBM Thinkpad X60), 3GB memory -* arm 32-bit (Raspberry PI 3) -* x86 64-bit (Intel NUC) - -### Operating systems - -On i686: -``` -Linux version 4.4.0-143-generic (buildd@lgw01-amd64-037) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.10) ) #169-Ubuntu SMP Thu Feb 7 07:56:51 UTC 2019 -``` - -## Results - -## References - -[RFC6241](https://tools.ietf.org/html/rfc6241) "Network Configuration Protocol (NETCONF)" -[RFC8040](https://tools.ietf.org/html/rfc8040) "RESTCONF Protocol" -[i686](https://ark.intel.com/content/www/us/en/ark/products/27235/intel-core-duo-processor-t2400-2m-cache-1-83-ghz-667-mhz-fsb.html) -[plot_perf.sh](../test/plot_perf.sh) Test script - - diff --git a/doc/scaling/clixon-commit-0.png b/doc/scaling/clixon-commit-0.png new file mode 100644 index 0000000000000000000000000000000000000000..cbab820a53fa9f5c76cf72566c34fc37744e724a GIT binary patch literal 6810 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 AtpET3 literal 0 HcmV?d00001 diff --git a/doc/scaling/clixon-delete-100.png b/doc/scaling/clixon-delete-100.png new file mode 100644 index 0000000000000000000000000000000000000000..9396c4831134584b701c0bf69f9e4f34ffc0ad0a GIT binary patch literal 8666 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 D#-31? literal 0 HcmV?d00001 diff --git a/doc/scaling/clixon-get-100.png b/doc/scaling/clixon-get-100.png new file mode 100644 index 0000000000000000000000000000000000000000..246768d4551267e7c77d797b04ee696835520cfe GIT binary patch literal 8588 zcmeAS@N?(olHy`uVBq!ia0y~yU}|7sV0^&A%)r3F#IJLWfq~H|z$e7@|Ns9C3=9hv zEC5jl7#JG;@b29K2Zj?T7#uSn{y(6=(BN>O;lDvbLpKA1Lj%MAhX3{duPoKl z`v0H7;lTw41_mi9DPv<}Utiy}w6yyA`k6ClZr;54z<~p2&YXGr^y&Zq|34TRO?Wq9 z?Tu$2LO(qC|Nq_HrvKXxu7CIL|8)k2{|pQ%3=IG4mAaMmy2HWzZb``k28IR(hO_PG zUNeB4_@Ck2#s4Cc|JMh6XlVHF%J8e9=;gGk56^n`ZqMG)#{Ko{odpdF|CchnYkD_@ z;qu)DcP9imgl6kEh%hvWh%`8eG&nRgBphf+_}>6>zr+6p4*wS<{9jP;|Gh25 zz`(#+;1OBOz`%C|gc+x5^GP!>C@^@sIEGZrc{`W8Y4s{0*RB4)jN5(Az5lbkfJv)q zQ4mMOi@5jq_V`5wWp36J=?D;EAOxHO`u?+3DsLBO)L6Ao=k>PF;Y-S`PCdPQ_a5h+ zu5I~U>ngAJZg1FnIZ*HYyKjCE{6Zd2|5xYlyJ1b+uDCy|)~r5%zBG7x?eeKVY_z$* z{@$c>`=R;W<$7J#i!rB^TZ!NH-?{nk37S>CLLDZVP3nmPKGt7#V_~s#mB*> zb^ml*Wb&_CbxpT_R@3+U&xI5BU%wCi`dGTR^~%FXuAweQ`wxmZh>6^E_%Al`){A=E z9doMIr7rXHh(7s#pZFq^L(AQ+xLpqX^mLWXRZ;e7zob`~OL4C}e6)Mji4T6Sc$YB* zJFeXLqo3i35TtMOcR?4AzEpk!*NrgO&wRh9Gfi7l>@e}i zNv@1g$CZmyriMwc?UBJruHeSNZOW%ny}k7nt9j9!r5YxuSNWe^ zQgv|d1((w7-nb7D9D~LL%Y98 zMq}gLb!)v32^fC-Ao#aQ7DvJnkSI>=eAt%F5R|x1M=@=dv%`c5+q8N_y??PWHMLwh zc1q~z?nA4(6;zjgTXA(jTsaWbp5)H%N@j+29hrR<1S;arI$D{ifOam%Y( zRYgFcgfz?ZVo5q{jIyA_b6G(^?q5a+s-TY zJe;qw;ec#%?Ej)$JuBy!*-bQE5V1XK@1oEBhr&LbZfKC%aJ}J`(zH{IXHQ4%zO$2U zPk7NWC3X@1)iMEo3xscf&|ti(eca)|X4gEiH1VIh9hu5*`a4#y_OFi4oGbA4lwrA; zKug*Wv1yMRKC=p#u5{TeWb-4`;lOcb#)C_rs6W^{$(Yab!BU3zJCN85s{Q zb#N%q{K;9d{z*?J-wZ!)jvpNcaVj$>&!69*Aei~=iSvPW?gs&l{4aWrpiLwK7-#A8PTNopHWk+U!iLEryC;-^}?l zeRI7e7e|v3XNJ#7^)n~y18;QIOA34_+LnDlxcE~bGgG)4<8{jg|L4>^X?Wg!N9T;$ z&365X%vC?yHGlrz5^DH9(8A}@O2r3k>-Bdv z3KYygD%bjWvek@+awf|J7S>Zwf3JFdqc}}Mzbb5j_#CGL53S1nd$P@*+IU{f!2dH- zefr8Qhm)_g`rb&@cP=^h_hEO@hYufI^(H=g`1I$+eIBY#3%7~!B%S&2^HEM%#(75e zX&2|+;jCQBDJ*hvdrCa>Hjo1}>ZUQBvO7^!dRE>1;PtKhU%P(Xy-hC9OsX#Qw0C~d zs=Dw~ua?O4=W9|d+THPVV%EKGgjU2<}`R;XSJ^2 z=ed2$>*v_<{^hqgbYt-b+4(2_pIF6m@$-XI|BhD$)^uhpKb2;+KzjKr@9)Pr4V;;u zeVAK&`oCqJ)bYoUK3tJ0`1Za@CSYF4=^Z(lB?`ye^JdA%`dr#olYamHx_^!bv=3!` zy&uXn%lO0n(zRa;N*P_>n#Wy}`aP-p{hQf`t}Wl$lWulUy#N2&>;wMG?&V%-SY&-O z?9k_Us+${e3W3$c6)vh1AkLf*7my~h8cucAPJmco})`nv|fBfHXSpDm~wisjg zF8TH=$Gj%j>(4kKT$r`Z*gmOVuW zlo>zo5)x{xVL7u&ye5Lt=|Ih*^y_~p&!;Lo4t2EHfi33iqXK;gEz zbKcwrzpwHvXVj|ld<|~Jt8Wq%_~+`Dd-T@V!%wbA{+;@2C-cS1iL)C*t>1dx4R`&1 zjrU6Y%U3-tTk>YVl#b<`;m-HsRs18Ff~`*b_Q>4%dh4Ck;U&|(w3qC^Wd4&Q&y?E)?@Ox7amw&T<@T2n%-{*yExlV7JIq%}*}7}TlQHyDX7kl>0Rzjm%e#Z8*=_i`|Yrl+`6tU-!(^5&qpM7 z;xU7Oy_a_06+P~Hphz;|o=KmW?e3=nGq^MSj@M6j?z-M@IcbebukG3^+HZG9)TZwj z*=83gGmVMwe`BRf_~wtQe~KNt>Q@#!d`!5~5Nn&0z4X@V%E=2kznrZubuGI%g!<(T69{jvF#$~0|`9pG1BZZv%(V> z-+JU9D-sK^f-x9I??3a$bZ%viuPAmQ;n)%3G-1@42Yr~UY z{p%Z2S59N;ky3kiyS?k&{=c%wV`Qvz&;I^I<{to*r7xdTH z6d!PLIFOJU7ca-=bU-d=KYW|=XNK`7#RQGA=?l*^yw&^}nEu51K){Fa z^Sd1O9iH-Mh4Ck2kqMc6KXw)Is;IBweq!x&a{0Lv;(vD5Ydu#^E|6!8&pcEiHt~k| ziG2R$PlD5*)PE7)QKxnM@uPy-YBTDYZO*sF=}f4%P@Qk3T)*PKLPAiH(1ke@>upr) z%iaD3+}^qA>q@=HdQ8tA+;`)xumLIi|ISq5u*`?4YCZLWCsiIOvD?g<#z55OlnH@j58bAe#i)XkeT`8_?nv+8XA_LIAYIH zp=Whp=7NBL-ai+8dNOH2Lo#)kAovzazo zPS^fFfw_u-aqnq|0|HOf6~v9QKx%DIp0+nTdyI?Y1t$wnuv$~YYFnjz4;io zB&PZJ&MiONWiB*-V7m2P!=zk(v1_`~Zd?05S3dBX!|1IEY+;ml`d zf|M;^nXj5q_HSc`*_syd4KaJyN5pyR7#O9uZF%vA<;=PL>D(Ic9pBoYOxSE>a9O6n z+2Kip+!67B=+_$}zc$61YhRgH_f|fqnqzz9LvOnY+mw0!UCfv*T2$9je17sw=CZZ> zrkpUE6XW!vGk!NqZU6=1gQZXQNiy3j zD0IeLtq$Rnu(DESEdI*RROJ%>dx`NW#_y~yd@mT28JD};G0BBA2wZ6X!?LF@M>^r$ z2C)z9Ol(Y+3bwlf-Y@^rSg+X7@a07`Q(UQD|6gaYrsHu2!i=)7>^WXA#xrsUa<1Aq z4{LYXPi9WI~^432W;54Oo(#$ z{)?Zf>VZze_3nD_-&)~?i#}&GCyu)Ur7H`wVkd4GSVTl>8O=+=q@!0rZ(=w;!f&IM7a5UyLnYR z&Mjcw=gE9V^4MX6>)aLc|MMm=yD>7UDU~zo?-VMkz4HGdm2Bh_~)-h z(j77vn9~_o?|S8xe7#@!&2BL(_x&LUroU)8!q3cjmsLdVBq!&&{JUYTsn?yJzYzcK z*tL7>A)kdlZUO&yCEnk9!SYut)4lV1q@Oe-^X|Bn`})P&=^!)e+Zz5~YT4P?;LNw< zUiLGn=zcx+nx&k|9|b-vv`o18B{ecw?eLp7Q7iav-dyt`J$d?W{o4nB^KX%5jMv=$ z=6Cy9uEiU-XFudPu4a+?+j@eYg0seF#?I>xZ>`$!ChCREUnR!LWe$^WmN(06s0)7I zzWF^!P>M15m-?ILhJ*ZbHol8`@n_le5~sBPkLpwZUva)-aHeoQWAiwftD zo%fITz0Z3c`8?jY%<0~Dk%hnIze-aGQ=GwvB4)1vgJ(h>lD)@C=)c~Ye+mkgzm_!^o?Ci!|ANGA`c(?-Po(Z@Em*;zT$8%( zm;ROp>8tWACAM0BVqfmQ7`*Mk$J_UppWk__F`IXX(1+?*Ozi60>SfaYDzdA%ZqM`o zEc;q3riO9-{!g>_HcYg+D^#%8HF4p(#un@8yUpX0FRuGc3X>e1C_xuYV@xcE$S=N=ZpU2?~tC@;7k`an79*KyDNe~J%qbTj9d7-ZDERIu&;5T! zGm0iCTXIM|){5zoGe~Bf$SkyYy_yosyXGU3jSsn5mbl*E%_aUQ#_ESV=i~hs1d0yG zu$W};XNaHa{nwqtIJ6#IK=C--V4G3>`SfgcE{QAl+mg?-onXwoINdTK;LtDePiG%} z)L`tj<=S1=apBa?mUY&uZ#gTz#aR7%F81t=SNI9W!`vBf0wbblN+nd2uhf znA#xa|DkQ#ew-3@a!v=>Ec(CnTRvosc^4RQZoQTJ{Oki4%9-20=--KUeeP-bps0Oa z;DKe=jSe@zsN&}N@5I3PyFC5o%MSq#r)^q-pI?eS&}^{nnYiTw_LVGC)_kkFdt*(& z{pAHaBx8TS4R^dJ?w=d>W6|`j_J6oP>BV2E71 z?QmPJ^-)(xUF{QvT_PW|UZ?qA^v|^K zka^N@{9^qw_mAhl{xgjGaFdy%LZ((EVB3Cs>*_{;qYg!-sr{PrLWm)x7$=-+uF{$7%@;f)ZYT zzrPp!RVY}XIbXq^vEQ%f=>E4#x3AY!)}$^xWN<)fe#7jB|TKh7(TtJtJF|3Mnlr9ZpB-q?IF^y^Q*tLBG%4z&Dxy8GHJ_Hd30 zo%ssO8m`51Z(nR3*6>aE^Ch{tT{0^{jjl)w^`9()tXtkk?U4L?`(xUJ-p~KFwrM+f zPn5`BYa4sOzO=Hrpj3HLLvCn9neLswx=(2dr??DW)EsHhnXj;@q1yV|jX0}nV}pbH zjT1MixVrxS{@wkW{;CG+>av@0xAscjV0eJ_Hee{j5rVxD)FQTD4d z-;0*G*FQrV!ns%Ehy4)zkiF4x;oFC&e|2B?{_|y%_~kN|KjJyF4;t|Q%nP$vvAv@A za9S%@5}(Gbg#V2W*PmVg{I;&MUT3vb@v7@+&Nu}Usc zi7Vs&)&lEaoQ$%6Kl4wpmP^yIDPHxhf)gZc{A>H-hH3VnPV8UEadv-uxzn`T?u^gR ze#UV3X)HJ&Rqrpp;e+fAXJh3N%_1LH~uB9weOO=)zZYysj*CP2iJ}t%E?#j{m(G+z7)Ea5wpG%BsQkKKy$80+s_@8vFej@xqAi&4@gV2Rk2DUd>XU+bky6Su+ zheo>7?=MXc=PQ>Pmu2r$=hRrPP*LK<_a}HR|FM%dQ!O7%J-d#Nn=$pSyZDC__TGZ! z?{pNzf3k#g|FB)_{$wKaJFXvJCfW->E;BXz#JNtaAa2rhod^5s_sPHPtGK)F+YA|l zw)z$Sc%GD+W}W>mz<71io^won^-tE-e^XM)o5H+ocI1PDSD7wlA9(oZ{g3VI{_U7G zQK6@(e`Wo3?jNx=lid>Cwz0arwq6ig-J^ZrG^@-z=S3z5g??;59%aG*S8fusmIqVR z-`&^cp3ln(DXS6g`or?))~f&8mhUa_Id`c2;hhV4lV>*Q7ygTvW@?LhZ>i7txx|0c z%!b+81vVM?HYZ9w7o3({S6_en{Qm2;4E<^q*}RL|Z-oE&nmhB_b4#ur7a1$h^yYv5 zJ84Rgz=b}4-XB}dK4kQNmVVl>W_?z`&s3EK)!cu!u3_%KeXp9MB1zCL?N499D#qB~ z3PE;v52!DxTQ2_a@xk>%{?)4HjN<3>*Uvv{F8?>^%5(4C)An!p-`3h-&wTv&Wa;}o zoU`73YGY`9C>%c+SQMV@9X`Ff0|q6 zlNobYe{kgxo*%!RE8o?PsqgLkx!-2^ZR3j9DS5l+&*|U;vtLXv5Vh+Ue3U!0?(Npt zYfS${+L`=hCrHN@dz-J4aX4_;%q*Qzw(4MWeO)o1#j}Qgi%)459Z20RmA^)%so|K~ z%*&I*^aNw|`Q|RNE^FXE|Eqs?!}DjSj*Ay8?(KD|VC?lZx;lB^?e2Fs8224a+U@;R zqSX3axke3#enPb9Es*wD#<|)D)?AO;y=M078;c9{Ht}|A{yfI!`O34_>wqRB&nHIx z2e%Z(`sG~DF-h&dv_ie~a`w!JO1o||oLM2iKU3?s>$kh!RlPciSjh@S@HqB%{_5Z%$hplV=&3M)@Z~E0ke@%Nn z@3B>i;k;;mGQ@vZ*nT@BcYC3Nr}FNM{&&_m{>?Bu`^hB#$*b&x?0X-Ge}88FRVajU zl672+?gKmhb9?)z7HUqiUmX8Bt|xJ`;h`9ZW&dKYT>l{N(bf=maqYj@YQ68fr2b9U z;NuTuzqjzJ=>x3=0q6H|oJnPp3#(qpawj`gF=5e*|1oAsy(g=9SA8%H6P;b$EAel0 z468Te(;J_UX*FbeT-f>bjo*((nF%6)@4vq9en8t=;mc_$J+;{B7aSduzFgmb_Lu(A zonLmWc{t%btJjN~GY`0f8OwzZ++eDTyY}J4bA^P1VF%_)8L0i68GoqeUEBwef^5|n z9ToR?{@8KZm3I*;M(DRp?K%Rd&_)ppIx)Oq4Ugx)6J}R zR(>~IwVipt&{atRga0qwd*3N-e0*!ey+(JAj1oOQ;eFO+855b~#Q%nCsmIsK3CnA*D5OJ}5N<2irL zi{-B;yh}Z@{M8ZGl1u8^Z7ZL@OI`J?m?`QOXNKLeut%3x1xsEsuhemqI1;~S){=a= zi@l$>Us-5v&(o1!bg8#`;-ZflpFghD^qij~c46pAddXM(cn;foJe0aVa)jI1LOy3P-(|CQrq5lpt2|)m@8-4@79FlB+%6iIzZ^Dpn)qvS=E*C^_V#EfY5U!Z zSGJQ`>T|XHiLk=|+Zo^ZO;7EppX`^!qA;r=)L$=Xn(j&;^?%x`TsP{2&bmpwh;u99 zowaZAKU3$Uo*K%(+_wlT6dyQWFDFp&SMTte@JI3dX>@vr#?tKYarlOk8yVJw4G*;<(!(q?Y#pf7r zud#HsGP z;$76Szq4chyA~GAg6g~wcDmeN$EcrK=#!b)5*R! zg%x{_bN(oaX`ARf-Dpd`P&M;l` z)y-cj@s36Od|Q0jR`+0?}FN0%E=g`cq;p z_o{^33D}>!+H#~R;KjrSjx)6r4(@jPomjCfk2hc2R1-DHoQ4B_LSjFw#N;~ dZf9_?`o}E!G0Y*xs`(a3!qe5yWt~$(699qNNQ3|Y literal 0 HcmV?d00001 diff --git a/doc/scaling/clixon-put-0.png b/doc/scaling/clixon-put-0.png new file mode 100644 index 0000000000000000000000000000000000000000..2e9acc152748845a538a3a750a1c58b6934cc911 GIT binary patch literal 7917 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 V!iUz{6})mF2~Sr)mvv4FO#r~Rw1ofw literal 0 HcmV?d00001 diff --git a/doc/scaling/large-lists.md b/doc/scaling/large-lists.md new file mode 100644 index 00000000..b36f9f2c --- /dev/null +++ b/doc/scaling/large-lists.md @@ -0,0 +1,131 @@ +# Large lists in Clixon + + * [Background](#background) + * [Overview](#overview) + * [Test descriptions](#test-descriptions) + * [Results](#results) + * [References](#references) + +## Background + +CIixon can handle large configurations. Here, large number of elements +in a "flat" list is presented. There are other scaling usecases, +such as large configuratin "depth", large number of requesting +clients, etc. + +## Overview + +The basic case is a large list, according to the following Yang specification: +``` + container x { + description "top-level container"; + list y { + description "List with potential large number of elements"; + key "a"; + leaf a { + description "key in list"; + type int32; + } + leaf b { + description "payload data"; + type string; + } + } + } +``` +where `a` is a unique key and `b` is a payload, useful in replace operations. + +XML lists with `N` elements are generated based on +this configuration, eg for `N=10`: +``` + 00 + 11 + 22 + 33 + 44 + 55 + 66 + 77 + 88 + 99 +``` + +Requests are either made over the _whole_ dataset, or for one specific element. The following example shows a Restconf GET operation of a single element: +``` + curl -X GET http://localhost/restconf/data/scaling:x/y=3 + {"scaling:y": [{"a": 3,"b": "3"}]} + +``` + +Operations of single elements (transactions) are made in a burst of +random elements, typically 100. + +## Tests + +All details of the setup are in the [test script](../../test/plot_perf.sh). + +### Testcases + +All tests measure the "real" time of a command on a lightly loaded +machine using the Linux command `time(1)`. + +The following tests were made (for each architecture and protocol): +* Write `N` entries in one single operation. (With an empty datastore) +* Read `N` entries in one single operation. (With a datastore of `N` entries) +* Commit `N` entries (With a candidate of `N` entries and empty running) +* Read 1 entry (In a datastore of `N` entries) +* Write/Replace 1 entry (In a datastore of `N` entries) +* Delete 1 entry (In a datastore of `N` entries) + +The tests are made using Netconf and Restconf, except commit which is made only for Netconf. + +### Architecture and OS + +The tests were made on the following hardware, all running Ubuntu Linux: +* i686: dual Intel Core Duo processor (IBM Thinkpad X60) +* arm: ARMv7 Processor rev 5 (v7l) (Raspberry PI 2 Model B) +* x86-64: Intel Quad-core I5-8259U (Intel NUC Coffee Lake) + +i686: Ubuntu 16.04.6 LTS +``` +Linux version 4.4.0-143-generic (buildd@lgw01-amd64-037) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.10) ) #169-Ubuntu SMP Thu Feb 7 07:56:51 UTC 2019 +``` + +Arm : Raspbian GNU/Linux 9 +``` + +Linux version 4.14.79-v7+ (dc4@dc4-XPS13-9333) (gcc version 4.9.3 (crosstool-NG crosstool-ng-1.22.0-88-g8460611)) #1159 SMP Sun Nov 4 17:50:20 GMT 2018 +``` + +x86_64: Ubuntu 18.04.1 LTS +``` +inux version 4.15.0-47-generic (buildd@lgw01-amd64-001) (gcc version 7.3.0 (Ubuntu 7.3.0-16ubuntu3)) #50-Ubuntu SMP Wed Mar 13 10:44:52 UTC 2019 +``` + +## Results + +![Get config](clixon-get-0.png "Get config") + +![Put config](clixon-put-0.png "Put config") + +![Commit config](clixon-commit-0.png "Commit config") + +![Get single entry](clixon-get-100.png "Get single entry") + +![Put single entry](clixon-put-100.png "Put single entry") + +![Delete single entry](clixon-delete-100.png "Delete single entry") + +## Discussion + + + + +## References + +[RFC6241](https://tools.ietf.org/html/rfc6241) "Network Configuration Protocol (NETCONF)" +[RFC8040](https://tools.ietf.org/html/rfc8040) "RESTCONF Protocol" +[i686](https://ark.intel.com/content/www/us/en/ark/products/27235/intel-core-duo-processor-t2400-2m-cache-1-83-ghz-667-mhz-fsb.html) +[plot_perf.sh](../test/plot_perf.sh) Test script + + diff --git a/example/README.md b/example/README.md index 58cdc401..66149d8b 100644 --- a/example/README.md +++ b/example/README.md @@ -1,6 +1,5 @@ # Clixon examples Clixon have the following examples: - * [Main example](main/README.md) * [Hello world](hello/README.md) - \ No newline at end of file + * [Main example](main/README.md) diff --git a/test/plot_perf.sh b/test/plot_perf.sh index 5cbc3cc7..e4a95acf 100755 --- a/test/plot_perf.sh +++ b/test/plot_perf.sh @@ -1,39 +1,34 @@ #!/bin/bash -# Transactions per second for large lists read/write plotter using gnuplot -# What do I want to plot? -# First: on i32, i64, arm32 -# PART 1: Basic load -# 1. How long to write 100K entries? -# - netconf / restconf -# - list / leaf-list -# 2. How long to read 100K entries? -# - netconf/ restconf -# - list / leaf-list -# 3. How long to commit 100K entries? (netconf) -# - list / leaf-list -# -# PART 2: Load 100K entries. Commit. -# 4. How many read operations per second? -# - netconf/ restconf -# - list / leaf-list -# 5. How many write operations per second? -# - netconf / restconf -# - list / leaf-list -# 6. How may delete operations per second? -# - netconf / restconf -# - list / leaf-list -# The script uses bash builtin "time" command which is somewhat difficult to -# understand. See: https://linux.die.net/man/1/bash # pipelines -# You essentially have to do: { time stuff; } 2>&1 -# See: https://stackoverflow.com/questions/26784870/parsing-the-output-of-bashs-time-builtin +# Performance of large lists. See large-lists.md +# The parameters are shown below (under Default values) +# Examples +# 1. run all measurements up to 10000 entris collect all results in /tmp/plots +# run=true plot=false to=10000 resdir=/tmp/plots ./plot_perf.sh +# 2. Use existing data plot and show on X11 +# run=false plot=true resdir=/tmp/plots term=x11 ./plot_perf.sh +# 3. Use existing data plot i686 and armv7l data as png +# archs="i686 armv7l" run=false plot=true resdir=/tmp/plots term=png ./plot_perf.sh # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi -# op from step to reqs -to=1000 -step=100 -reqs=100 +arch=$(arch) +# Default values +: ${to:=5000} # Max N +: ${step=1000} # Iterate in steps (also starting point) +: ${reqs=100} # Number of requests in each burst +: ${run:=true} # run tests (or skip them). If false just plot +: ${term:=x11} # x11 interactive, alt: png +: ${resdir=$dir} # Result dir (both data and gnuplot) +: ${plot=false} # Result dir (both data and gnuplot) +: ${archs=$arch} # Plotting can be made for many architectures (not run) + +# 0 prefix to protect against shell dynamic binding) +to0=$to +step0=$step +reqs0=$reqs + +ext=$term # gnuplot output file extenstion # Global variables APPNAME=example @@ -42,6 +37,13 @@ fyang=$dir/plot.yang fxml=$dir/data.xml fjson=$dir/data.json +# Resultdir - if different from $dir that gets erased +#resdir=$dir + +if [ ! -d $resdir ]; then + mkdir $resdir +fi + # For memcheck # clixon_netconf="valgrind --leak-check=full --show-leak-kinds=all clixon_netconf" # clixon_netconf="valgrind --tool=callgrind clixon_netconf @@ -53,19 +55,20 @@ module scaling{ namespace "urn:example:clixon"; prefix sc; container x { - list y { - key "a"; - leaf a { - type uint32; + description "top-level container"; + list y { + description "List with potential large number of elements"; + key "a"; + leaf a { + description "key in list"; + type int32; + } + leaf b { + description "payload data"; + type string; + } } - leaf b { - type string; - } - } - leaf-list c { - type string; - } - } + } } EOF @@ -109,48 +112,49 @@ genfile(){ # where proto is one of: # netconf, restconf # where op is one of: -# writeall readall commitall read write +# get put delete commit runnet(){ op=$1 - n=$2 # Number of entries in DB + nr=$2 # Number of entries in DB (keep diff from n due to shell dynamic binding) reqs=$3 - echo -n "$n " >> $dir/$op-netconf-$reqs + file=$resdir/$op-netconf-$reqs-$arch + echo -n "$nr " >> $file case $op in - write) + put) if [ $reqs = 0 ]; then # Write all in one go - genfile $n netconf; - { time -p cat $fxml | $clixon_netconf -qf $cfg -y $fyang ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-netconf-$reqs + genfile $nr netconf; + { time -p cat $fxml | $clixon_netconf -qf $cfg -y $fyang ; } 2>&1 | awk '/real/ {print $2}' | tr , . >> $file else # reqs != 0 { time -p for (( i=0; i<$reqs; i++ )); do - rnd=$(( ( RANDOM % $n ) )); + rnd=$(( ( RANDOM % $nr ) )); echo "$rnd$rnd]]>]]>"; - done | $clixon_netconf -qf $cfg -y $fyang ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-netconf-$reqs + done | $clixon_netconf -qf $cfg -y $fyang > /dev/null; } 2>&1 | awk '/real/ {print $2}' | tr , . >> $file fi ;; - read) + get) if [ $reqs = 0 ]; then # Read all in one go - { time -p echo "]]>]]>" | $clixon_netconf -qf $cfg -y $fyang > /dev/null ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-netconf-$reqs + { time -p echo "]]>]]>" | $clixon_netconf -qf $cfg -y $fyang > /dev/null ; } 2>&1 | awk '/real/ {print $2}' | tr , . >> $file else # reqs != 0 { time -p for (( i=0; i<$reqs; i++ )); do rnd=$(( ( RANDOM % $nr ) )) - echo "$rnd$rnd]]>]]>" -done | $clixon_netconf -qf $cfg -y $fyang; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-netconf-$reqs + echo "$rnd$rnd]]>]]>" + done | $clixon_netconf -qf $cfg -y $fyang > /dev/null; } 2>&1 | awk '/real/ {print $2}' | tr , . >> $file fi ;; delete) { time -p for (( i=0; i<$reqs; i++ )); do rnd=$(( ( RANDOM % $nr ) )) - echo "$rnd$rnd]]>]]>" -done | $clixon_netconf -qf $cfg -y $fyang; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-netconf-$reqs + echo "$rnd$rnd]]>]]>" +done | $clixon_netconf -qf $cfg -y $fyang; } 2>&1 | awk '/real/ {print $2}' | tr , . >> $file ;; commit) - { time -p echo "]]>]]>" | $clixon_netconf -qf $cfg -y $fyang > /dev/null ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-netconf-$reqs + { time -p echo "]]>]]>" | $clixon_netconf -qf $cfg -y $fyang > /dev/null ; } 2>&1 | awk '/real/ {print $2}' | tr , . >> $file ;; *) err "Operation not supported" "$op" exit - ;; + ;; esac } @@ -159,43 +163,43 @@ done | $clixon_netconf -qf $cfg -y $fyang; } 2>&1 | awk '/real/ {print $2}' >> $ # where proto is one of: # netconf, restconf # where op is one of: -# writeall readall commitall read write +# get put delete runrest(){ op=$1 - n=$2 # Number of entries in DB + nr=$2 # Number of entries in DB reqs=$3 - echo -n "$n " >> $dir/$op-restconf-$reqs + file=$resdir/$op-restconf-$reqs-$arch + echo -n "$nr " >> $file case $op in - write) + put) if [ $reqs = 0 ]; then # Write all in one go - genfile $n restconf + genfile $nr restconf # restconf @- means from stdin - { time -p curl -sS -X PUT -d @$fjson http://localhost/restconf/data/scaling:x ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-restconf-$reqs + { time -p curl -sS -X PUT -d @$fjson http://localhost/restconf/data/scaling:x ; } 2>&1 | awk '/real/ {print $2}' | tr , . >> $file else # Small requests { time -p for (( i=0; i<$reqs; i++ )); do - rnd=$(( ( RANDOM % $n ) )); + rnd=$(( ( RANDOM % $nr ) )); curl -sS -X PUT http://localhost/restconf/data/scaling:x/y=$rnd -d "{\"scaling:y\":{\"a\":$rnd,\"b\":\"$rnd\"}}" - done ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-restconf-$reqs + done ; } 2>&1 | awk '/real/ {print $2}' | tr , .>> $file # fi ;; - read) + get) if [ $reqs = 0 ]; then # Read all in one go - { time -p curl -sS -X GET http://localhost/restconf/data/scaling:x > /dev/null; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-restconf-$reqs + { time -p curl -sS -X GET http://localhost/restconf/data/scaling:x > /dev/null; } 2>&1 | awk '/real/ {print $2}' | tr , . >> $file else # Small requests { time -p for (( i=0; i<$reqs; i++ )); do - rnd=$(( ( RANDOM % $n ) )); + rnd=$(( ( RANDOM % $nr ) )); curl -sS -X GET http://localhost/restconf/data/scaling:x/y=$rnd - done ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-restconf-$reqs + done ; } 2>&1 | awk '/real/ {print $2}' | tr , .>> $file fi ;; - delete) + delete) { time -p for (( i=0; i<$reqs; i++ )); do - rnd=$(( ( RANDOM % $n ) )); + rnd=$(( ( RANDOM % $nr ) )); curl -sS -X GET http://localhost/restconf/data/scaling:x/y=$rnd - done ; } 2>&1 | awk '/real/ {print $2}' >> $dir/$op-restconf-$reqs - + done ; } 2>&1 | awk '/real/ {print $2}' | tr , .>> $file ;; *) err "Operation not supported" "$op" @@ -246,8 +250,8 @@ plot(){ fi # reset file - new "Create file $dir/$op-$proto-$reqs" - echo "" > $dir/$op-$proto-$reqs + new "Create file $resdir/$op-$proto-$reqs-$arch" + echo -n "" > $resdir/$op-$proto-$reqs-$arch for (( n=$from; n<=$to; n=$n+$step )); do reset if [ $can = n ]; then @@ -256,7 +260,7 @@ plot(){ commit fi fi - new "$op-$proto-$reqs $n" + new "$op-$proto-$reqs-$arch $n" if [ $proto = netconf ]; then runnet $op $n $reqs else @@ -266,6 +270,7 @@ plot(){ echo # newline } +if $run; then new "test params: -f $cfg -y $fyang" if [ $BE -ne 0 ]; then new "kill old backend" @@ -286,26 +291,35 @@ start_restconf -f $cfg -y $fyang new "waiting" sleep $RCWAIT + +to=$to0 +step=$step0 +reqs=$reqs0 + +# Put all tests for proto in netconf restconf; do - new "$proto write all entries to candidate (restconf:running)" - plot write $proto $step $step $to 0 0 0 # all candidate 0 running 0 + new "$proto put all entries to candidate (restconf:running)" + plot put $proto $step $step $to 0 0 0 # all candidate 0 running 0 done +# Get all tests for proto in netconf restconf; do - new "$proto read all entries from running" - plot read netconf $step $step $to 0 n n # start w full datastore + new "$proto get all entries from running" + plot get $proto $step $step $to 0 n n # start w full datastore done +# Netconf commit all new "Netconf commit all entries from candidate to running" plot commit netconf $step $step $to 0 n 0 # candidate full running empty -reqs=100 +# Transactions get/put/delete +reqs=$reqs0 for proto in netconf restconf; do - new "$proto read $reqs from full database" - plot read $proto $step $step $to $reqs n n + new "$proto get $reqs from full database" + plot get $proto $step $step $to $reqs n n - new "$proto Write $reqs to full database(replace / alter values)" - plot write $proto $step $step $to $reqs n n + new "$proto put $reqs to full database(replace / alter values)" + plot put $proto $step $step $to $reqs n n new "$proto delete $reqs from full database(replace / alter values)" plot delete $proto $step $step $to $reqs n n @@ -324,15 +338,127 @@ if [ $BE -ne 0 ]; then # kill backend stop_backend -f $cfg fi +fi # if run + +if $plot; then + +# 1. Get config +gplot="" +for a in $archs; do + gplot="$gplot \"$resdir/get-restconf-0-$a\" title \"rc-$a\", \"$resdir/get-netconf-0-$a\" title \"nc-$a\"," +done -arch=$(arch) gnuplot -persist < Date: Wed, 17 Apr 2019 17:41:51 +0200 Subject: [PATCH 05/13] scaling doc update --- doc/scaling/large-lists.md | 129 +++++++++++++++++++++++++++++-------- 1 file changed, 101 insertions(+), 28 deletions(-) diff --git a/doc/scaling/large-lists.md b/doc/scaling/large-lists.md index b36f9f2c..333e4ac1 100644 --- a/doc/scaling/large-lists.md +++ b/doc/scaling/large-lists.md @@ -1,19 +1,25 @@ # Large lists in Clixon - * [Background](#background) - * [Overview](#overview) - * [Test descriptions](#test-descriptions) - * [Results](#results) - * [References](#references) +Olof Hagsand, 2019-04-17 -## Background + * [1. Background](#1-background) + * [2. Overview](#2-overview) + * [3. Tests](#3-tests) + * [4. Results](#4-results) + * [5. Discussion](#5-discussion) + * [6. Future work](#6-future-work) + * [7. References](#7-references) + +## 1. Background CIixon can handle large configurations. Here, large number of elements in a "flat" list is presented. There are other scaling usecases, such as large configuratin "depth", large number of requesting clients, etc. -## Overview +Thanks to [Netgate](www.netgate.com) for supporting this work. + +## 2.Overview The basic case is a large list, according to the following Yang specification: ``` @@ -60,7 +66,7 @@ Requests are either made over the _whole_ dataset, or for one specific element. Operations of single elements (transactions) are made in a burst of random elements, typically 100. -## Tests +## 3. Tests All details of the setup are in the [test script](../../test/plot_perf.sh). @@ -82,27 +88,39 @@ The tests are made using Netconf and Restconf, except commit which is made only ### Architecture and OS The tests were made on the following hardware, all running Ubuntu Linux: -* i686: dual Intel Core Duo processor (IBM Thinkpad X60) -* arm: ARMv7 Processor rev 5 (v7l) (Raspberry PI 2 Model B) -* x86-64: Intel Quad-core I5-8259U (Intel NUC Coffee Lake) -i686: Ubuntu 16.04.6 LTS -``` -Linux version 4.4.0-143-generic (buildd@lgw01-amd64-037) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.10) ) #169-Ubuntu SMP Thu Feb 7 07:56:51 UTC 2019 -``` +#### i686 -Arm : Raspbian GNU/Linux 9 -``` +* IBM Thinkpad X60 +* Dual Intel Core Duo processor +* Ubuntu 16.04.6 LTS +* Linux version 4.4.0-143-generic (buildd@lgw01-amd64-037) + * gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.10) + * #169-Ubuntu SMP Thu Feb 7 07:56:51 UTC 2019 -Linux version 4.14.79-v7+ (dc4@dc4-XPS13-9333) (gcc version 4.9.3 (crosstool-NG crosstool-ng-1.22.0-88-g8460611)) #1159 SMP Sun Nov 4 17:50:20 GMT 2018 -``` +#### ARM -x86_64: Ubuntu 18.04.1 LTS -``` -inux version 4.15.0-47-generic (buildd@lgw01-amd64-001) (gcc version 7.3.0 (Ubuntu 7.3.0-16ubuntu3)) #50-Ubuntu SMP Wed Mar 13 10:44:52 UTC 2019 -``` +* Raspberry PI 2 Model B +* ARMv7 Processor rev 5 (v7l) +* Raspbian GNU/Linux 9 +* Linux version 4.14.79-v7+ (dc4@dc4-XPS13-9333) + * gcc version 4.9.3 (crosstool-NG crosstool-ng-1.22.0-88-g8460611)) + * #1159 SMP Sun Nov 4 17:50:20 GMT 2018 -## Results +#### x86_64 + +* Intel NUC Coffee Lake +* Intel Quad-core I5-8259U +* Ubuntu 18.04.1 LTS +* Linux version 4.15.0-47-generic (buildd@lgw01-amd64-001) + * gcc version 7.3.0 (Ubuntu 7.3.0-16ubuntu3) + * #50-Ubuntu SMP Wed Mar 13 10:44:52 UTC 2019 + +## 4. Results + +### Access of the whole datastore + +This section shows the results of the measurements as defined in [Tests](#tests). ![Get config](clixon-get-0.png "Get config") @@ -110,22 +128,77 @@ inux version 4.15.0-47-generic (buildd@lgw01-amd64-001) (gcc version 7.3.0 (Ubun ![Commit config](clixon-commit-0.png "Commit config") +### Access of single entries + ![Get single entry](clixon-get-100.png "Get single entry") ![Put single entry](clixon-put-100.png "Put single entry") ![Delete single entry](clixon-delete-100.png "Delete single entry") -## Discussion +### Profiling +An example profiling of the most demanding case was made: Put single restconf for with 5000 existing entries. +The tool used is valgrind/callgrind with the following approximate result: +* from_rpc_callback 100% + * from_client_commit 65% + * candidate_commit 65% + * from_validate_common 30% + * xml_diff 13% + * xml_yang_validate_all 10% + * xmldb_copy 29% + * xml_copy 22% + * from_client_edit_config 35% + * xmldb_put 35% + * clicon_xml2file 30% + * fprintf 13% + * xml_chardata_encode 12% +It can be seen that most cycles are spend in file copying and writing +to file which explains the linear behaviour. +## 5. Discussion -## References +All measurements show clear performance differences between the +architectures, which was expected. + +By looking at top and other tools, it seems clear that the main +bootleneck is CPU for the `clixon_backend`. The clients, eg `nginx`, +clixon_restconf` and `clixon_netconf` seem negligable. Memory +footprint is also not limiting. + +Accessing the whole configuration is similar between protocols and +linear in time. This is to be expected since the tranfer is dependent +on the size of the database. + +Accessing a single entry is also similar in all cases and shows large +differences. As expected, the CPU architecture is significant, but +there is also a large difference between Netconf and Restconf. + +The Netconf and restconf setups differ somewhat which may explain +differences in performance. Primarily, the Netconf input is piped to a +single Netconf client while a curl is re-started for each Restconf +call. Also, the Restconf PUT includes a commit. + +Further, the single entry access is linear wrt number of entries. This +means it is possible to run large scale applications on high +performance CPUs, such as 100K entries on a x86_64 in the results, but +it also means that very large lists may not be supported, and that the +system degrades with the size of the lists. + +Examining the profiling of the most demanding Restconf PUT case + +Concluding, the + +## 6. Future work + +It is recommended that the following items should be studied: +* Try to improve the access of single list elements, to sub-linear perforamnce. +* CLI access on large lists (not included in this study) + +## 7. References [RFC6241](https://tools.ietf.org/html/rfc6241) "Network Configuration Protocol (NETCONF)" [RFC8040](https://tools.ietf.org/html/rfc8040) "RESTCONF Protocol" [i686](https://ark.intel.com/content/www/us/en/ark/products/27235/intel-core-duo-processor-t2400-2m-cache-1-83-ghz-667-mhz-fsb.html) [plot_perf.sh](../test/plot_perf.sh) Test script - - From 6c2e880ab99d0414856f4ab56da5256c8fef7203 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Wed, 17 Apr 2019 17:53:40 +0200 Subject: [PATCH 06/13] Scaling doc update --- doc/scaling/large-lists.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/doc/scaling/large-lists.md b/doc/scaling/large-lists.md index 333e4ac1..229561ab 100644 --- a/doc/scaling/large-lists.md +++ b/doc/scaling/large-lists.md @@ -139,7 +139,7 @@ This section shows the results of the measurements as defined in [Tests](#tests) ### Profiling An example profiling of the most demanding case was made: Put single restconf for with 5000 existing entries. -The tool used is valgrind/callgrind with the following approximate result: +The tool used is valgrind/callgrind with the following approximate result (percentage of total cycles): * from_rpc_callback 100% * from_client_commit 65% * candidate_commit 65% @@ -155,7 +155,14 @@ The tool used is valgrind/callgrind with the following approximate result: * xml_chardata_encode 12% It can be seen that most cycles are spend in file copying and writing -to file which explains the linear behaviour. +the existing datastore to file. This explains the linear behaviour, ie +the larger existing datastore, the larger number of cycles spent. + +Why is the existing datastore accessed in this way? When a small PUT request is received, several things happen: +* The existing candidate database is modified with the change and written to disk. (35%) +* The difference between candidate and running is computed (13%) +* The new candidate is validated (10%) +* The candidate db is copied to running (29%) ## 5. Discussion @@ -186,19 +193,18 @@ performance CPUs, such as 100K entries on a x86_64 in the results, but it also means that very large lists may not be supported, and that the system degrades with the size of the lists. -Examining the profiling of the most demanding Restconf PUT case +Examining the profiling of the most demanding Restconf PUT case, most +cycles are spent on handling writing and copying the existing datastore. Concluding, the ## 6. Future work -It is recommended that the following items should be studied: -* Try to improve the access of single list elements, to sub-linear perforamnce. +* Improve access of single list elements to sub-linear performance. * CLI access on large lists (not included in this study) ## 7. References -[RFC6241](https://tools.ietf.org/html/rfc6241) "Network Configuration Protocol (NETCONF)" -[RFC8040](https://tools.ietf.org/html/rfc8040) "RESTCONF Protocol" -[i686](https://ark.intel.com/content/www/us/en/ark/products/27235/intel-core-duo-processor-t2400-2m-cache-1-83-ghz-667-mhz-fsb.html) -[plot_perf.sh](../test/plot_perf.sh) Test script +* [RFC6241](https://tools.ietf.org/html/rfc6241) "Network Configuration Protocol (NETCONF)" +* [RFC8040](https://tools.ietf.org/html/rfc8040) "RESTCONF Protocol" +* [plot_perf.sh](../../test/plot_perf.sh) Test script From b714eff077f584b78c36a054f655dbe41f89d0bf Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Thu, 18 Apr 2019 12:20:31 +0200 Subject: [PATCH 07/13] Failure in startup with -m startup or running left running_db cleared. --- CHANGELOG.md | 2 + apps/backend/backend_main.c | 19 +++++- apps/backend/backend_startup.c | 16 +++-- test/test_startup.sh | 103 ++++++++++++++++++++++++++++----- 4 files changed, 119 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57215d1a..c6eba715 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -134,6 +134,8 @@ * Added libgen.h for baseline() ### Corrected Bugs +* Failure in startup with -m startup or running left running_db cleared. + * Running-db should not be changed on failure. Unless failure-db defined. Or if SEGV, etc. In those cases, tmp_db should include the original running-db. * Backend plugin returning NULL was still installed - is now logged and skipped. * [Parent list key is not validated if not provided via RESTCONF #83](https://github.com/clicon/clixon/issues/83), thanks achernavin22. * [Invalid JSON if GET /operations via RESTCONF #82](https://github.com/clicon/clixon/issues/82), thanks achernavin22 diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index 1cac8931..7583ea5a 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -638,16 +638,33 @@ main(int argc, status = STARTUP_OK; break; case SM_RUNNING: /* Use running as startup */ - /* Copy original running to startup and treat as startup */ + /* Copy original running to tmp as backup (restore if error) */ if (xmldb_copy(h, "running", "tmp") < 0) goto done; + /* [Delete and] create running db */ + if (startup_db_reset(h, "running") < 0) + goto done; ret = startup_mode_startup(h, "tmp", cbret); + /* If ret fails, copy tmp back to running */ + if (ret != 1) + if (xmldb_copy(h, "tmp", "running") < 0) + goto done; if (ret2status(ret, &status) < 0) goto done; break; case SM_STARTUP: + /* Copy original running to tmp as backup (restore if error) */ + if (xmldb_copy(h, "running", "tmp") < 0) + goto done; + /* [Delete and] create running db */ + if (startup_db_reset(h, "running") < 0) + goto done; /* Load and commit from startup */ ret = startup_mode_startup(h, "startup", cbret); + /* If ret fails, copy tmp back to running */ + if (ret != 1) + if (xmldb_copy(h, "tmp", "running") < 0) + goto done; if (ret2status(ret, &status) < 0) goto done; /* if status = STARTUP_INVALID, cbret contains info */ diff --git a/apps/backend/backend_startup.c b/apps/backend/backend_startup.c index 9c9ab382..fe0d5ba8 100644 --- a/apps/backend/backend_startup.c +++ b/apps/backend/backend_startup.c @@ -150,9 +150,6 @@ startup_mode_startup(clicon_handle h, int retval = -1; int ret; - /* [Delete and] create running db */ - if (startup_db_reset(h, "running") < 0) - goto done; /* Load plugins and call plugin_init() */ if (backend_plugin_initiate(h) != 0) goto done; @@ -299,15 +296,22 @@ startup_failsafe(clicon_handle h) clicon_err(OE_XML, errno, "cbuf_new"); goto done; } - if (startup_db_reset(h, "running") < 0) - goto done; if ((ret = xmldb_exists(h, db)) < 0) goto done; if (ret == 0){ /* No it does not exist, fail */ clicon_err(OE_DB, 0, "Startup failed and no Failsafe database found, exiting"); goto done; } - if ((ret = candidate_commit(h, db, cbret)) < 0) /* diff */ + /* Copy original running to tmp as backup (restore if error) */ + if (xmldb_copy(h, "running", "tmp") < 0) + goto done; + if (startup_db_reset(h, "running") < 0) + goto done; + ret = candidate_commit(h, db, cbret); + if (ret != 1) + if (xmldb_copy(h, "tmp", "running") < 0) + goto done; + if (ret < 0) goto done; if (ret == 0){ clicon_err(OE_DB, 0, "Startup failed, Failsafe database validation failed %s", cbuf_get(cbret)); diff --git a/test/test_startup.sh b/test/test_startup.sh index 9457e4ea..ac4be92f 100755 --- a/test/test_startup.sh +++ b/test/test_startup.sh @@ -6,6 +6,9 @@ # - An extra xml configuration file starts with an "extra" interface # - running db starts with a "run" interface # - startup db starts with a "start" interface +# There is also an "invalid" XML and a "broken" XML +# There are two steps, first run through everything OK +# Then try with invalid and borken XML and ensure the backend quits and all is untouched # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi @@ -36,27 +39,37 @@ cat < $cfg EOF -# Create running-db containin the interface "run" +# Create running-db containin the interface "run" OK runvar='runex:ethtrue' -# Create startup-db containing the interface "startup" +# Create startup-db containing the interface "startup" OK startvar='startupex:ethtrue' -# extra +# extra OK extravar='extraex:ethtrue' +# invalid (contains ), but OK XML syntax +invalidvar='invalidex:ethtrue' + +# Broken XML (contains ) +brokenvar='brokenex:ethtrue' + # Create a pre-set running, startup and (extra) config. # The configs are identified by an interface called run, startup, extra. # Depending on startup mode (init, none, running, or startup) # expect different output of an initial get-config of running testrun(){ mode=$1 - exprun=$2 # expected running_db after startup + rdb=$2 # running db at start + sdb=$3 # startup db at start + edb=$4 # extradb at start + exprun=$5 # expected running_db after startup + sudo rm -f $dir/*_db - echo "$runvar" > $dir/running_db - echo "$startvar" > $dir/startup_db - echo "$extravar" > $dir/extra_db + echo "$rdb" > $dir/running_db + echo "$sdb" > $dir/startup_db + echo "$edb" > $dir/extra_db if [ $BE -ne 0 ]; then # Bring your own backend # kill old backend (if any) @@ -81,7 +94,7 @@ testrun(){ expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' "^$exprun]]>]]>$" new "Startup test for $mode mode, check startup is untouched" - expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' "^$startvar]]>]]>$" + expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' "^$sdb]]>]]>$" new "Kill backend" # Check if premature kill @@ -93,17 +106,79 @@ testrun(){ stop_backend -f $cfg } # testrun + +# The backend should fail with 255 and all db:s should be unaffected +testfail(){ + mode=$1 + rdb=$2 # running db at start + sdb=$3 # startup db at start + edb=$4 # extradb at start + + sudo rm -f $dir/*_db + + echo "$rdb" > $dir/running_db + echo "$sdb" > $dir/startup_db + echo "$edb" > $dir/extra_db + + # kill old backend (if any) + new "kill old backend" + sudo clixon_backend -zf $cfg + if [ $? -ne 0 ]; then + err + fi + new "start backend -f $cfg -s $mode -c $dir/extra_db" + ret=$(start_backend -1 -s $mode -f $cfg -c $dir/extra_db 2> /dev/null) + r=$? + if [ $r -ne 255 ]; then + err "Unexpected retval" $r + fi + # permission kludges + sudo chmod 666 $dir/running_db + sudo chmod 666 $dir/startup_db + new "Checking running unchanged" + ret=$(diff $dir/running_db <(echo "$rdb")) + if [ $? -ne 0 ]; then + err "$rdb" "$ret" + fi + new "Checking startup unchanged" + ret=$(diff $dir/startup_db <(echo "$sdb")) + if [ $? -ne 0 ]; then + err "$sdb" "$ret" + fi + + new "Checking extra unchanged" + ret=$(diff $dir/extra_db <(echo "$edb")) + if [ $? -ne 0 ]; then + err "$edb" "$ret" + fi +} + +# 1. Try different modes on OK running/startup/extra # Init mode: delete running and reload from scratch (just extra) -testrun init "$extravar" +testrun init "$runvar" "$startvar" "$extravar" "$extravar" # None mode: do nothing, running remains -testrun none "$runvar" +testrun none "$runvar" "$startvar" "$extravar" "$runvar" # Running mode: keep running but load also extra -testrun running 'extraex:ethtruerunex:ethtrue' +testrun running "$runvar" "$startvar" "$extravar" 'extraex:ethtruerunex:ethtrue' # Startup mode: scratch running, load startup with extra on top -testrun startup 'extraex:ethtruestartupex:ethtrue' +testrun startup "$runvar" "$startvar" "$extravar" 'extraex:ethtruestartupex:ethtrue' -echo $dir -#rm -rf $dir +# 2. Try different modes on Invalid running/startup/extra WITHOUT failsafe +# ensure all db:s are unchanged after failure. + +new "Test invalid running in running mode" +testfail running "$invalidvar" "$startvar" "$extravar" + +new "Run invalid startup in startup mode" +testfail startup "$runvar" "$invalidvar" "$extravar" + +new "Test broken running in running mode" +testfail running "$brokenvar" "$startvar" "$extravar" + +new "Run broken startup in startup mode" +testfail startup "$runvar" "$brokenvar" "$extravar" + +rm -rf $dir From 728c97ab6df70d7ff5f7640f2e74350ee011c27d Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Thu, 18 Apr 2019 15:38:15 +0200 Subject: [PATCH 08/13] Failure in startup - modified to handle memory exhaustion when loading file --- apps/backend/backend_commit.c | 14 ++++++++++++++ apps/backend/backend_main.c | 6 ------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/backend/backend_commit.c b/apps/backend/backend_commit.c index 22839104..a5fe8845 100644 --- a/apps/backend/backend_commit.c +++ b/apps/backend/backend_commit.c @@ -180,6 +180,7 @@ startup_common(clicon_handle h, if (clicon_option_bool(h, "CLICON_XMLDB_MODSTATE")) if ((msd = modstate_diff_new()) == NULL) goto done; + clicon_debug(1, "Reading startup config from %s", db); if (xmldb_get(h, db, "/", &xt, msd) < 0) goto done; if (msd){ @@ -211,6 +212,7 @@ startup_common(clicon_handle h, /* 5. Make generic validation on all new or changed data. Note this is only call that uses 3-values */ + clicon_debug(1, "Validating startup %s", db); if ((ret = generic_validate(yspec, td, cbret)) < 0) goto done; if (ret == 0) @@ -282,6 +284,7 @@ startup_validate(clicon_handle h, * @retval -1 Error - or validation failed (but cbret not set) * @retval 0 Validation failed (with cbret set) * @retval 1 Validation OK + * Only called from startup_mode_startup */ int startup_commit(clicon_handle h, @@ -292,6 +295,10 @@ startup_commit(clicon_handle h, int ret; transaction_data_t *td = NULL; + if (strcmp(db,"running")==0){ + clicon_err(OE_FATAL, 0, "Invalid startup db: %s", db); + goto done; + } /* Handcraft a transition with only target and add trees */ if ((td = transaction_new()) == NULL) goto done; @@ -302,6 +309,13 @@ startup_commit(clicon_handle h, /* 8. Call plugin transaction commit callbacks */ if (plugin_transaction_commit(h, td) < 0) goto done; + /* [Delete and] create running db */ + if (xmldb_exists(h, "running") == 1){ + if (xmldb_delete(h, "running") != 0 && errno != ENOENT) + goto done;; + } + if (xmldb_create(h, "running") < 0) + goto done; /* 9, write (potentially modified) tree to running * XXX note here startup is copied to candidate, which may confuse everything */ diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index 7583ea5a..9d02568b 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -641,9 +641,6 @@ main(int argc, /* Copy original running to tmp as backup (restore if error) */ if (xmldb_copy(h, "running", "tmp") < 0) goto done; - /* [Delete and] create running db */ - if (startup_db_reset(h, "running") < 0) - goto done; ret = startup_mode_startup(h, "tmp", cbret); /* If ret fails, copy tmp back to running */ if (ret != 1) @@ -656,9 +653,6 @@ main(int argc, /* Copy original running to tmp as backup (restore if error) */ if (xmldb_copy(h, "running", "tmp") < 0) goto done; - /* [Delete and] create running db */ - if (startup_db_reset(h, "running") < 0) - goto done; /* Load and commit from startup */ ret = startup_mode_startup(h, "startup", cbret); /* If ret fails, copy tmp back to running */ From 8c36083e1693fc98c183ed0ee944af382a014dc2 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Fri, 19 Apr 2019 16:01:39 +0200 Subject: [PATCH 09/13] * A yang type regex cache added, this helps the performance by avoiding re-running the `regcomp` command on every iteration. * An XML namespace cache added (see `xml2ns()`) * Better performance of XML whitespace parsing/scanning. --- apps/backend/backend_commit.c | 7 ++- apps/backend/backend_startup.c | 8 +++ lib/clixon/clixon_yang.h | 4 ++ lib/src/clixon_xml.c | 13 +++- lib/src/clixon_xml_parse.l | 6 +- lib/src/clixon_xml_parse.y | 56 +++++++++++++++-- lib/src/clixon_yang.c | 27 ++++++++ lib/src/clixon_yang_type.c | 112 ++++++++++++++++++++++++++++++--- 8 files changed, 214 insertions(+), 19 deletions(-) diff --git a/apps/backend/backend_commit.c b/apps/backend/backend_commit.c index a5fe8845..3565ea83 100644 --- a/apps/backend/backend_commit.c +++ b/apps/backend/backend_commit.c @@ -146,7 +146,7 @@ generic_validate(yang_stmt *yspec, * and call application callback validations. * @param[in] h Clicon handle * @param[in] db The startup database. The wanted backend state - * @param[out] xtr Transformed XML + * @param[in] td Transaction * @param[out] cbret CLIgen buffer w error stmt if retval = 0 * @retval -1 Error - or validation failed (but cbret not set) * @retval 0 Validation failed (with cbret set) @@ -183,6 +183,10 @@ startup_common(clicon_handle h, clicon_debug(1, "Reading startup config from %s", db); if (xmldb_get(h, db, "/", &xt, msd) < 0) goto done; + if (xml_child_nr(xt) == 0){ /* If empty skip */ + td->td_target = xt; + goto ok; + } if (msd){ if ((ret = clixon_module_upgrade(h, xt, msd, cbret)) < 0) goto done; @@ -225,6 +229,7 @@ startup_common(clicon_handle h, /* 7. Call plugin transaction complete callbacks */ if (plugin_transaction_complete(h, td) < 0) goto done; + ok: retval = 1; done: if (msd) diff --git a/apps/backend/backend_startup.c b/apps/backend/backend_startup.c index fe0d5ba8..34102e7a 100644 --- a/apps/backend/backend_startup.c +++ b/apps/backend/backend_startup.c @@ -114,6 +114,7 @@ db_merge(clicon_handle h, /*! Clixon startup startup mode: Commit startup configuration into running state * @param[in] h Clixon handle + * @param[in] db tmp or startup * @param[out] cbret If status is invalid contains error message * @retval -1 Error * @retval 0 Validation failed @@ -150,6 +151,10 @@ startup_mode_startup(clicon_handle h, int retval = -1; int ret; + if (strcmp(db, "running")==0){ + clicon_err(OE_FATAL, 0, "Invalid startup db: %s", db); + goto done; + } /* Load plugins and call plugin_init() */ if (backend_plugin_initiate(h) != 0) goto done; @@ -255,6 +260,8 @@ startup_extraxml(clicon_handle h, goto done; if (ret == 0) goto fail; + if (xt==NULL /* || xml_child_nr(xt)==0 */ ) /* This gives SEGV in test_feature */ + goto ok; /* Write (potentially modified) xml tree xt back to tmp */ if ((ret = xmldb_put(h, "tmp", OP_REPLACE, xt, @@ -265,6 +272,7 @@ startup_extraxml(clicon_handle h, goto fail; if (ret == 0) goto fail; + ok: retval = 1; done: if (xt) diff --git a/lib/clixon/clixon_yang.h b/lib/clixon/clixon_yang.h index f9472bba..81153b72 100644 --- a/lib/clixon/clixon_yang.h +++ b/lib/clixon/clixon_yang.h @@ -218,7 +218,9 @@ struct yang_stmt{ Y_TYPE & identity: store all derived types */ yang_type_cache *ys_typecache; /* If ys_keyword==Y_TYPE, cache all typedef data except unions */ + void *ys_regex_cache; /* regex cache */ int _ys_vector_i; /* internal use: yn_each */ + }; typedef int (yang_applyfn_t)(yang_stmt *ys, void *arg); @@ -232,6 +234,8 @@ enum rfc_6020 yang_keyword_get(yang_stmt *ys); char *yang_argument_get(yang_stmt *ys); cg_var *yang_cv_get(yang_stmt *ys); cvec *yang_cvec_get(yang_stmt *ys); +void *yang_regex_cache_get(yang_stmt *ys); +int yang_regex_cache_set(yang_stmt *ys, void *regex); /* Other functions */ yang_stmt *yspec_new(void); diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index b19909a6..50f6f7e7 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -127,6 +127,7 @@ struct xml{ reference, dont free */ cg_var *x_cv; /* Cached value as cligen variable (eg xml_cmp) */ + char *x_ns_cache; /* Cached namespace */ int _x_vector_i; /* internal use: xml_child_each */ int _x_i; /* internal use for sorting: see xml_enumerate and xml_cmp */ @@ -234,6 +235,7 @@ xml_prefix_set(cxobj *xn, * @retval 0 OK * @retval -1 Error * @see xmlns_check XXX can these be merged? + * @note, this function uses a cache. Any case where cache should be cleared? */ int xml2ns(cxobj *x, @@ -241,9 +243,11 @@ xml2ns(cxobj *x, char **namespace) { int retval = -1; - char *ns; + char *ns = NULL; cxobj *xp; + if ((ns = x->x_ns_cache) != NULL) + goto ok; if (prefix != NULL) /* xmlns:="" */ ns = xml_find_type_value(x, "xmlns", prefix, CX_ATTR); else /* xmlns="" */ @@ -261,6 +265,11 @@ xml2ns(cxobj *x, ns = DEFAULT_XML_RPC_NAMESPACE; #endif } + if (ns && (x->x_ns_cache = strdup(ns)) == NULL){ + clicon_err(OE_XML, errno, "strdup"); + goto done; + } + ok: if (namespace) *namespace = ns; retval = 0; @@ -1359,6 +1368,8 @@ xml_free(cxobj *x) free(x->x_childvec); if (x->x_cv) cv_free(x->x_cv); + if (x->x_ns_cache) + free(x->x_ns_cache); free(x); return 0; } diff --git a/lib/src/clixon_xml_parse.l b/lib/src/clixon_xml_parse.l index 35a9d5af..8d1826c2 100644 --- a/lib/src/clixon_xml_parse.l +++ b/lib/src/clixon_xml_parse.l @@ -132,10 +132,10 @@ ncname {namestart}{namechar}* "\< { BEGIN(START); return *clixon_xml_parsetext; } & { _YA->ya_lex_state =STATEA;BEGIN(AMPERSAND);} -[ \t] { clixon_xml_parselval.string = yytext;return WHITESPACE; } -\r\n { clixon_xml_parselval.string = "\n";return WHITESPACE; } +[ \t]+ { clixon_xml_parselval.string = yytext;return WHITESPACE; } +\r\n { clixon_xml_parselval.string = "\n"; _YA->ya_linenum++; return WHITESPACE; } \r { clixon_xml_parselval.string = "\n";return WHITESPACE; } -\n { clixon_xml_parselval.string = yytext; _YA->ya_linenum++;return WHITESPACE; } +\n { clixon_xml_parselval.string = "\n"; _YA->ya_linenum++;return WHITESPACE; } . { clixon_xml_parselval.string = yytext; return CHARDATA; } /* @see xml_chardata_encode */ diff --git a/lib/src/clixon_xml_parse.y b/lib/src/clixon_xml_parse.y index b5fcbc05..96876021 100644 --- a/lib/src/clixon_xml_parse.y +++ b/lib/src/clixon_xml_parse.y @@ -112,6 +112,44 @@ xml_parse_content(struct xml_parse_yacc_arg *ya, return retval; } +/*! Add whitespace + * If text, ie only body, keep as is. + * But if there is an element, then skip all whitespace. + */ +static int +xml_parse_whitespace(struct xml_parse_yacc_arg *ya, + char *str) +{ + cxobj *xn = ya->ya_xelement; + cxobj *xp = ya->ya_xparent; + int retval = -1; + int i; + + ya->ya_xelement = NULL; /* init */ + /* If there is an element already, only add one whitespace child + * otherwise, keep all whitespace. + */ +#if 1 + for (i=0; iya_xelement = xn; + ok: + retval = 0; + done: + return retval; +} + + static int xml_parse_version(struct xml_parse_yacc_arg *ya, char *ver) @@ -243,11 +281,17 @@ xml_parse_bslash1(struct xml_parse_yacc_arg *ya, while ((xc = xml_child_each(x, xc, CX_ELMNT)) != NULL) break; if (xc != NULL){ /* at least one element */ - xc = NULL; - while ((xc = xml_child_each(x, xc, CX_BODY)) != NULL) { - xml_purge(xc); - xc = NULL; /* reset iterator */ - } + int i; + for (i=0; i element"); } | pi { clicon_debug(2, "content -> pi"); } | CHARDATA { if (xml_parse_content(_YA, $1) < 0) YYABORT; clicon_debug(2, "content -> CHARDATA %s", $1); } - | WHITESPACE { if (xml_parse_content(_YA, $1) < 0) YYABORT; + | WHITESPACE { if (xml_parse_whitespace(_YA, $1) < 0) YYABORT; clicon_debug(2, "content -> WHITESPACE %s", $1); } | { clicon_debug(2, "content -> "); } ; diff --git a/lib/src/clixon_yang.c b/lib/src/clixon_yang.c index 5003c1a5..0e7d443b 100644 --- a/lib/src/clixon_yang.c +++ b/lib/src/clixon_yang.c @@ -69,6 +69,7 @@ #include #include #include +#include /* cligen */ #include @@ -222,6 +223,20 @@ yang_cvec_get(yang_stmt *ys) return ys->ys_cvec; } +void* +yang_regex_cache_get(yang_stmt *ys) +{ + return ys->ys_regex_cache; +} + +int +yang_regex_cache_set(yang_stmt *ys, + void *regex) +{ + ys->ys_regex_cache = regex; + return 0; +} + /* End access functions */ /*! Create new yang specification @@ -266,6 +281,17 @@ ys_new(enum rfc_6020 keyw) return ys; } + +static int +yang_regex_cache_free(yang_stmt *ys) +{ + if (ys->ys_regex_cache){ + regfree(ys->ys_regex_cache); + free(ys->ys_regex_cache); + } + return 0; +} + /*! Free a single yang statement */ static int ys_free1(yang_stmt *ys) @@ -280,6 +306,7 @@ ys_free1(yang_stmt *ys) cvec_free(ys->ys_cvec); if (ys->ys_typecache) yang_type_cache_free(ys->ys_typecache); + yang_regex_cache_free(ys); free(ys); return 0; } diff --git a/lib/src/clixon_yang_type.c b/lib/src/clixon_yang_type.c index 413d4e8e..483ed0ac 100644 --- a/lib/src/clixon_yang_type.c +++ b/lib/src/clixon_yang_type.c @@ -49,6 +49,7 @@ #include #include #include +#include #include /* cligen */ @@ -99,6 +100,74 @@ static const map_str2int ytmap[] = { {NULL, -1} }; +/*! Regular expression compiling + * @retval -1 Error + * @retval 0 regex problem (no match?) + * @retval 1 OK Match + * @see match_regexp the CLIgen original composite function + */ +static int +regex_compile(char *pattern0, + regex_t *re) +{ + int retval = -1; + char pattern[1024]; + // char errbuf[1024]; + int len0; + int status; + + len0 = strlen(pattern0); + if (len0 > sizeof(pattern)-5){ + clicon_err(OE_XML, EINVAL, "pattern too long"); + goto done; + } + strncpy(pattern, "^(", 2); + strncpy(pattern+2, pattern0, sizeof(pattern)-2); + strncat(pattern, ")$", sizeof(pattern)-len0-1); + if ((status = regcomp(re, pattern, REG_NOSUB|REG_EXTENDED)) != 0) { +#if 0 /* ignore error msg for now */ + regerror(status, re, errbuf, sizeof(errbuf)); +#endif + goto fail; + } + retval = 1; + done: + return retval; + fail: + retval = 0; + goto done; +} + +/*! Regular expression execution + * @retval -1 Error + * @retval 0 regex problem (no match?) + * @retval 1 OK Match + * @see match_regexp the CLIgen original composite function + */ +static int +regex_exec(regex_t *re, + char *string) +{ + int retval = -1; + int status; + // char errbuf[1024]; + + status = regexec(re, string, (size_t) 0, NULL, 0); + if (status != 0) { +#if 0 /* ignore error msg for now */ + regerror(status, re, errbuf, sizeof(errbuf)); +#endif + goto fail; + } + retval = 1; + done: + return retval; + fail: + retval = 0; + goto done; +} + + /* return 1 if built-in, 0 if not */ static int yang_builtin(char *type) @@ -525,14 +594,41 @@ cv_validate1(cg_var *cv, } if ((options & YANG_OPTIONS_PATTERN) != 0){ char *posix = NULL; - if (regexp_xsd2posix(pattern, &posix) < 0) - goto done; - if ((retval2 = match_regexp(str?str:"", posix)) < 0){ - clicon_err(OE_DB, 0, "match_regexp: %s", pattern); - return -1; + regex_t *re = NULL; + + if ((re = yang_regex_cache_get(yrestype)) == NULL){ + /* Transform to posix regex */ + if (regexp_xsd2posix(pattern, &posix) < 0) + goto done; + /* Create regex cache */ + if ((re = malloc(sizeof(*re))) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); + goto done; + } + memset(re, 0, sizeof(*re)); + /* Compute regex pattern for use in patterns */ + if ((retval2 = regex_compile(posix, re)) < 0) + goto done; + if (retval2 == 0){ + if (reason) + *reason = cligen_reason("regexp match fail: \"%s\" does not match %s", + str, pattern); + goto fail; + break; + } + yang_regex_cache_set(yrestype, re); + if (posix) + free(posix); + } + if ((retval2 = regex_exec(re, str?str:"")) < 0) + goto done; + if (retval2 == 0){ + if (reason) + *reason = cligen_reason("regexp match fail: \"%s\" does not match %s", + str, pattern); + goto fail; + break; } - if (posix) - free(posix); if (retval2 == 0){ if (reason) *reason = cligen_reason("regexp match fail: \"%s\" does not match %s", @@ -667,7 +763,7 @@ ys_cv_validate_union(yang_stmt *ys, /*! Validate cligen variable cv using yang statement as spec * * @param[in] cv A cligen variable to validate. This is a correctly parsed cv. - * @param[in] ys A yang statement, must be leaf of leaf-list. + * @param[in] ys A yang statement, must be leaf or leaf-list. * @param[out] reason If given, and if return value is 0, contains a malloced string * describing the reason why the validation failed. Must be freed. * @retval -1 Error (fatal), with errno set to indicate error From 6a0628141a6e71d328dfbab9a6849fcf2e700b83 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 21 Apr 2019 17:29:11 +0200 Subject: [PATCH 10/13] More performance tweaks --- CHANGELOG.md | 14 +++++++++----- apps/backend/backend_main.c | 4 +++- apps/backend/backend_startup.c | 2 +- doc/scaling/large-lists.md | 4 ++-- doc/startup.md | 6 ++++-- example/Makefile.in | 2 +- lib/src/clixon_datastore.c | 24 +++++++++++++----------- test/lib.sh | 1 + test/test_perf.sh | 30 +++++++++++++++++++++++++++++- 9 files changed, 63 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6eba715..5f929ddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,15 @@ * Two config options control: * CLICON_XML_CHANGELOG enables the yang changelog feature * CLICON_XML_CHANGELOG_FILE where the changelog resides +* Optimization work + * Improved performance of validation of (large) lists + * A scaling of [large lists](doc/scaling) report is added + * New xmldb_get1() returning actual cache - not a copy. This has lead to some householding instead of just deleting the copy + * xml_diff rewritten to work linearly instead of O(2) + * New xml_insert function using tree search. The new code uses this in insertion xmldb_put and defaults. (Note previous xml_insert renamed to xml_wrap_all) + * A yang type regex cache added, this helps the performance by avoiding re-running the `regcomp` command on every iteration. + * An XML namespace cache added (see `xml2ns()`) + * Better performance of XML whitespace parsing/scanning. ### API changes on existing features (you may need to change your code) @@ -102,12 +111,7 @@ ### Minor changes -* A scaling of [large lists](doc/scaling) report is added * A new "hello world" example is added -* Optimized validation of large lists - * New xmldb_get1() returning actual cache - not a copy. This has lead to some householding instead of just deleting the copy - * xml_diff rewritten to work linearly instead of O(2) - * New xml_insert function using tree search. The new code uses this in insertion xmldb_put and defaults. (Note previous xml_insert renamed to xml_wrap_all) * Experimental customized error output strings, see [lib/clixon/clixon_err_string.h] * Empty leaf values, eg are now checked at validation. * Empty values were skipped in validation. diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index 9d02568b..658447e8 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -694,8 +694,9 @@ main(int argc, /* Call backend plugin_start with user -- options */ if (clixon_plugin_start(h) < 0) goto done; + /* -1 option to run only once */ if (once) - goto done; + goto ok; /* Daemonize and initiate logging. Note error is initiated here to make demonized errors OK. Before this stage, errors are logged on stderr @@ -733,6 +734,7 @@ main(int argc, goto done; if (event_loop() < 0) goto done; + ok: retval = 0; done: if (cbret) diff --git a/apps/backend/backend_startup.c b/apps/backend/backend_startup.c index 34102e7a..367ed627 100644 --- a/apps/backend/backend_startup.c +++ b/apps/backend/backend_startup.c @@ -260,7 +260,7 @@ startup_extraxml(clicon_handle h, goto done; if (ret == 0) goto fail; - if (xt==NULL /* || xml_child_nr(xt)==0 */ ) /* This gives SEGV in test_feature */ + if (xt==NULL || xml_child_nr(xt)==0) goto ok; /* Write (potentially modified) xml tree xt back to tmp */ diff --git a/doc/scaling/large-lists.md b/doc/scaling/large-lists.md index 229561ab..35424dc0 100644 --- a/doc/scaling/large-lists.md +++ b/doc/scaling/large-lists.md @@ -12,14 +12,14 @@ Olof Hagsand, 2019-04-17 ## 1. Background -CIixon can handle large configurations. Here, large number of elements +Clixon can handle large configurations. Here, large number of elements in a "flat" list is presented. There are other scaling usecases, such as large configuratin "depth", large number of requesting clients, etc. Thanks to [Netgate](www.netgate.com) for supporting this work. -## 2.Overview +## 2. Overview The basic case is a large list, according to the following Yang specification: ``` diff --git a/doc/startup.md b/doc/startup.md index 921c0072..d9937cc0 100644 --- a/doc/startup.md +++ b/doc/startup.md @@ -254,7 +254,8 @@ When the startup process is completed, a startup status is set and is accessible If the startup fails, the backend looks for a `failsafe` configuration in `CLICON_XMLDB_DIR/failsafe_db`. If such a config is not found, the -backend terminates. +backend terminates. In this mode, running and startup mode should be +unchanged. If the failsafe is found, the failsafe config is loaded and committed into the running db. @@ -405,6 +406,7 @@ running |--------+------------> GOTO EXTRA XML ### Running mode +On failure, running is restored to initial state ``` running ----+ |----------+--------> GOTO EXTRA XML \ copy parse validate OK / commit @@ -420,7 +422,7 @@ running |--------+------------> GOTO EXTRA XML startup -------+--+-------+------------+ ``` -### Failure +### Failure if failsafe ``` failsafe ----------------------+ reset \ commit diff --git a/example/Makefile.in b/example/Makefile.in index 90a0aa19..3c9a2a89 100644 --- a/example/Makefile.in +++ b/example/Makefile.in @@ -40,7 +40,7 @@ LIBS = @LIBS@ SHELL = /bin/sh -SUBDIRS = main +SUBDIRS = main hello .PHONY: all clean depend install $(SUBDIRS) diff --git a/lib/src/clixon_datastore.c b/lib/src/clixon_datastore.c index 149cf49d..278a0023 100644 --- a/lib/src/clixon_datastore.c +++ b/lib/src/clixon_datastore.c @@ -188,14 +188,15 @@ xmldb_copy(clicon_handle h, int retval = -1; char *fromfile = NULL; char *tofile = NULL; - db_elmnt *de1 = NULL; - db_elmnt *de2 = NULL; + db_elmnt *de1 = NULL; /* from */ + db_elmnt *de2 = NULL; /* to */ db_elmnt de0 = {0,}; - cxobj *x1 = NULL; - cxobj *x2 = NULL; + cxobj *x1 = NULL; /* from */ + cxobj *x2 = NULL; /* to */ /* XXX lock */ if (clicon_option_bool(h, "CLICON_XMLDB_CACHE")){ + /* Copy in-memory cache */ /* 1. "to" xml tree in x1 */ if ((de1 = clicon_db_elmnt_get(h, from)) != NULL) x1 = de1->de_xml; @@ -208,7 +209,7 @@ xmldb_copy(clicon_handle h, xml_free(x2); x2 = NULL; } - else if (x2 == NULL){ /* create x2 and copy x1 to it */ + else if (x2 == NULL){ /* create x2 and copy from x1 */ if ((x2 = xml_new(xml_name(x1), NULL, xml_spec(x1))) == NULL) goto done; if (xml_copy(x1, x2) < 0) @@ -221,12 +222,13 @@ xmldb_copy(clicon_handle h, if (xml_copy(x1, x2) < 0) goto done; } - if (x1 || x2){ - if (de2) - de0 = *de2; - de0.de_xml = x2; /* The new tree */ - clicon_db_elmnt_set(h, to, &de0); - } + /* always set cache although not strictly necessary in case 1 + * above, but logic gets complicated due to differences with + * de and de->de_xml */ + if (de2) + de0 = *de2; + de0.de_xml = x2; /* The new tree */ + clicon_db_elmnt_set(h, to, &de0); } /* Copy the files themselves (above only in-memory cache) */ if (xmldb_db2file(h, from, &fromfile) < 0) diff --git a/test/lib.sh b/test/lib.sh index 61769b4c..f2a9d32c 100755 --- a/test/lib.sh +++ b/test/lib.sh @@ -201,6 +201,7 @@ new(){ # - expected command return value (0 if OK) # - expected stdout outcome, # - expected2 stdout outcome, +# Example: expectfn "$clixon_cli -1 -f $cfg show conf cli" 0 "^$" expectfn(){ cmd=$1 retval=$2 diff --git a/test/test_perf.sh b/test/test_perf.sh index e3513a52..e0fe91af 100755 --- a/test/test_perf.sh +++ b/test/test_perf.sh @@ -5,7 +5,7 @@ s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi # Number of list/leaf-list entries in file -: ${perfnr:=2000} +: ${perfnr:=10000} # Number of requests made get/put : ${perfreq:=100} @@ -50,9 +50,36 @@ cat < $cfg false $dir false + example + /usr/local/lib/example/cli + /usr/local/lib/example/clispec + 1 + VARS + 0 EOF +# Try startup mode w startup +for mode in startup running; do + file=$dir/${mode}_db + sudo touch $file + sudo chmod 666 $file + new "generate large startup config ($file) with $perfnr list entries in mode $mode" + echo -n "" > $file + for (( i=0; i<$perfnr; i++ )); do + echo -n "$i$i" >> $file + done + echo "" >> $file + + new "Startup backend once -s $mode -f $cfg -y $fyang" + # Cannot use start_backend here due to expected error case + time sudo $clixon_backend -F1 -D $DBG -s $mode -f $cfg -y $fyang # 2> /dev/null +done + +new "Startup backend once -s $mode -f $cfg -y $fyang" +# Cannot use start_backend here due to expected error case +time sudo $clixon_backend -F1 -D $DBG -s $mode -f $cfg -y $fyang # 2> /dev/null + new "test params: -f $cfg -y $fyang" if [ $BE -ne 0 ]; then new "kill old backend" @@ -184,4 +211,5 @@ fi # kill backend stop_backend -f $cfg + rm -rf $dir From 77ad42f1ce98c2e59146b837ff1653f30b813459 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Tue, 23 Apr 2019 11:57:18 +0200 Subject: [PATCH 11/13] startup measurements: test, plot and report --- doc/scaling/clixon-startup.png | Bin 0 -> 6209 bytes doc/scaling/large-lists.md | 35 +++++-- test/plot_perf.sh | 168 +++++++++++++++++++++++---------- test/test_perf.sh | 10 +- 4 files changed, 149 insertions(+), 64 deletions(-) create mode 100644 doc/scaling/clixon-startup.png diff --git a/doc/scaling/clixon-startup.png b/doc/scaling/clixon-startup.png new file mode 100644 index 0000000000000000000000000000000000000000..5115bb67445df60f78ff69a4a44c4de562976145 GIT binary patch literal 6209 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 Q?HEYh)78&qol`;+02oshN&o-= literal 0 HcmV?d00001 diff --git a/doc/scaling/large-lists.md b/doc/scaling/large-lists.md index 35424dc0..7bb3653b 100644 --- a/doc/scaling/large-lists.md +++ b/doc/scaling/large-lists.md @@ -12,10 +12,17 @@ Olof Hagsand, 2019-04-17 ## 1. Background -Clixon can handle large configurations. Here, large number of elements -in a "flat" list is presented. There are other scaling usecases, -such as large configuratin "depth", large number of requesting -clients, etc. +Clixon can handle large configurations. Here, measurements using a +large number of elements in a simple "flat" list is analysed. This +includes starting up with alarge existing database; initializing an +empty database with a large number of entries, accessing single +entries with a large database, etc. + +In short, the results show a linear dependency on the number of +entries. This is OK for startup scenarions, but single-enrty (transactional) operations need improvement. + +There are other scaling usecases, such as large configuratin "depth", +large number of requesting clients, etc. Thanks to [Netgate](www.netgate.com) for supporting this work. @@ -41,7 +48,7 @@ The basic case is a large list, according to the following Yang specification: ``` where `a` is a unique key and `b` is a payload, useful in replace operations. -XML lists with `N` elements are generated based on +With this XML lists with `N` elements are generated based on this configuration, eg for `N=10`: ``` 00 @@ -66,6 +73,7 @@ Requests are either made over the _whole_ dataset, or for one specific element. Operations of single elements (transactions) are made in a burst of random elements, typically 100. + ## 3. Tests All details of the setup are in the [test script](../../test/plot_perf.sh). @@ -76,6 +84,7 @@ All tests measure the "real" time of a command on a lightly loaded machine using the Linux command `time(1)`. The following tests were made (for each architecture and protocol): +* Write `N` entries into the startup configuration. The clixon_backend was started with options `-1s startup`. * Write `N` entries in one single operation. (With an empty datastore) * Read `N` entries in one single operation. (With a datastore of `N` entries) * Commit `N` entries (With a candidate of `N` entries and empty running) @@ -83,7 +92,7 @@ The following tests were made (for each architecture and protocol): * Write/Replace 1 entry (In a datastore of `N` entries) * Delete 1 entry (In a datastore of `N` entries) -The tests are made using Netconf and Restconf, except commit which is made only for Netconf. +The tests are made using Netconf and Restconf, except commit which is made only for Netconf and startup where protocol is irrelevant. ### Architecture and OS @@ -118,9 +127,12 @@ The tests were made on the following hardware, all running Ubuntu Linux: ## 4. Results -### Access of the whole datastore - This section shows the results of the measurements as defined in [Tests](#tests). +### Startup + +![Startup](clixon-startup.png "Startup") + +### Access of the whole datastore ![Get config](clixon-get-0.png "Get config") @@ -196,11 +208,14 @@ system degrades with the size of the lists. Examining the profiling of the most demanding Restconf PUT case, most cycles are spent on handling writing and copying the existing datastore. -Concluding, the +Note that the experiments here contains _very_ simple +data-structures. A more realistic complex example will require more +CPU effort. Ad-hoc measurement of a more complex datastructure, +generated four times the duration of the simple yang model in this work. ## 6. Future work -* Improve access of single list elements to sub-linear performance. +* Improve access of individual elements to sub-linear performance. * CLI access on large lists (not included in this study) ## 7. References diff --git a/test/plot_perf.sh b/test/plot_perf.sh index e4a95acf..9d394746 100755 --- a/test/plot_perf.sh +++ b/test/plot_perf.sh @@ -270,78 +270,142 @@ plot(){ echo # newline } +# Run an operation, iterate from to in increment of +# Each operation do times +# args: +# =0 means all in one go +startup(){ + from=$1 + step=$2 + to=$3 + mode=startup + + if [ $# -ne 3 ]; then + exit "plot should be called with 3 arguments, got $#" + fi + + # gnuplot file + gfile=$resdir/startup-$arch + new "Create file $gfile" + echo -n "" > $gfile + + # Startup db: load with n entries + dbfile=$dir/${mode}_db + sudo touch $dbfile + sudo chmod 666 $dbfile + for (( n=$from; n<=$to; n=$n+$step )); do + new "startup-$arch $n" + new "Generate $n entries to $dbfile" + echo -n "" > $dbfile + for (( i=0; i<$n; i++ )); do + echo -n "$i$i" >> $dbfile + done + echo "" >> $dbfile + + new "Startup backend once -s $mode -f $cfg -y $fyang" + echo -n "$n " >> $gfile + { time -p sudo $clixon_backend -F1 -D $DBG -s $mode -f $cfg -y $fyang 2> /dev/null; } 2>&1 | awk '/real/ {print $2}' | tr , . >> $gfile + + done + echo # newline +} + if $run; then -new "test params: -f $cfg -y $fyang" -if [ $BE -ne 0 ]; then - new "kill old backend" - sudo clixon_backend -zf $cfg -y $fyang - if [ $? -ne 0 ]; then - err + + # Startup test before regular backend/restconf start since we only start + # backend a single time + startup $step $step $to + + new "test params: -f $cfg -y $fyang" + if [ $BE -ne 0 ]; then + new "kill old backend" + sudo clixon_backend -zf $cfg -y $fyang + if [ $? -ne 0 ]; then + err + fi + new "start backend -s init -f $cfg -y $fyang" + start_backend -s init -f $cfg -y $fyang fi - new "start backend -s init -f $cfg -y $fyang" - start_backend -s init -f $cfg -y $fyang -fi -new "kill old restconf daemon" -sudo pkill -u www-data -f "/www-data/clixon_restconf" + new "kill old restconf daemon" + sudo pkill -u www-data -f "/www-data/clixon_restconf" -new "start restconf daemon" -start_restconf -f $cfg -y $fyang + new "start restconf daemon" + start_restconf -f $cfg -y $fyang -new "waiting" -sleep $RCWAIT + new "waiting" + sleep $RCWAIT -to=$to0 -step=$step0 -reqs=$reqs0 + to=$to0 + step=$step0 + reqs=$reqs0 -# Put all tests -for proto in netconf restconf; do - new "$proto put all entries to candidate (restconf:running)" - plot put $proto $step $step $to 0 0 0 # all candidate 0 running 0 -done -# Get all tests -for proto in netconf restconf; do - new "$proto get all entries from running" - plot get $proto $step $step $to 0 n n # start w full datastore -done + # Put all tests + for proto in netconf restconf; do + new "$proto put all entries to candidate (restconf:running)" + plot put $proto $step $step $to 0 0 0 # all candidate 0 running 0 + done -# Netconf commit all -new "Netconf commit all entries from candidate to running" -plot commit netconf $step $step $to 0 n 0 # candidate full running empty + # Get all tests + for proto in netconf restconf; do + new "$proto get all entries from running" + plot get $proto $step $step $to 0 n n # start w full datastore + done -# Transactions get/put/delete -reqs=$reqs0 -for proto in netconf restconf; do - new "$proto get $reqs from full database" - plot get $proto $step $step $to $reqs n n + # Netconf commit all + new "Netconf commit all entries from candidate to running" + plot commit netconf $step $step $to 0 n 0 # candidate full running empty - new "$proto put $reqs to full database(replace / alter values)" - plot put $proto $step $step $to $reqs n n + # Transactions get/put/delete + reqs=$reqs0 + for proto in netconf restconf; do + new "$proto get $reqs from full database" + plot get $proto $step $step $to $reqs n n - new "$proto delete $reqs from full database(replace / alter values)" - plot delete $proto $step $step $to $reqs n n -done + new "$proto put $reqs to full database(replace / alter values)" + plot put $proto $step $step $to $reqs n n -new "Kill restconf daemon" -stop_restconf + new "$proto delete $reqs from full database(replace / alter values)" + plot delete $proto $step $step $to $reqs n n + done -if [ $BE -ne 0 ]; then - new "Kill backend" - # Check if premature kill - pid=`pgrep -u root -f clixon_backend` - if [ -z "$pid" ]; then - err "backend already dead" + new "Kill restconf daemon" + stop_restconf + + if [ $BE -ne 0 ]; then + new "Kill backend" + # Check if premature kill + pid=`pgrep -u root -f clixon_backend` + if [ -z "$pid" ]; then + err "backend already dead" + fi + # kill backend + stop_backend -f $cfg fi - # kill backend - stop_backend -f $cfg -fi fi # if run if $plot; then +# 0. Startup +gplot="" +for a in $archs; do + gplot="$gplot \"$resdir/startup-$a\" title \"startup-$a\"," +done + +gnuplot -persist < $cfg EOF +if [ $BE -ne 0 ]; then + new "kill old backend" + sudo clixon_backend -zf $cfg -y $fyang + if [ $? -ne 0 ]; then + err + fi +fi # Try startup mode w startup for mode in startup running; do file=$dir/${mode}_db @@ -113,7 +120,6 @@ new "netconf write large config" expecteof_file "/usr/bin/time -f %e $clixon_netconf -qf $cfg -y $fyang" "$fconfig" "^]]>]]>$" # Here, there are $perfnr entries in candidate - new "netconf write large config again" expecteof_file "/usr/bin/time -f %e $clixon_netconf -qf $cfg -y $fyang" "$fconfig" "^]]>]]>$" From 1e1eabbc06ffc29989e5f70ea492bd23fbb662e6 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Tue, 23 Apr 2019 12:53:29 +0200 Subject: [PATCH 12/13] Non linear str2int --- lib/clixon/clixon_string.h | 1 + lib/src/clixon_hash.c | 1 + lib/src/clixon_string.c | 61 +++++++++++++++++++++++++++++++++++++- lib/src/clixon_xml.c | 7 +++-- lib/src/clixon_yang_type.c | 45 ++++++++++++++++++++++------ util/clixon_util_xpath.c | 8 +---- 6 files changed, 104 insertions(+), 19 deletions(-) diff --git a/lib/clixon/clixon_string.h b/lib/clixon/clixon_string.h index f78ddbc9..eb106c10 100644 --- a/lib/clixon/clixon_string.h +++ b/lib/clixon/clixon_string.h @@ -86,6 +86,7 @@ int xml_chardata_encode(char **escp, char *fmt, ...); int uri_percent_decode(char *enc, char **str); const char *clicon_int2str(const map_str2int *mstab, int i); int clicon_str2int(const map_str2int *mstab, char *str); +int clicon_str2int_search(const map_str2int *mstab, char *str, int upper); int nodeid_split(char *nodeid, char **prefix, char **id); char *clixon_trim(char *str); int regexp_xsd2posix(char *xsd, char **posix); diff --git a/lib/src/clixon_hash.c b/lib/src/clixon_hash.c index fa47bfcc..b14e10ad 100644 --- a/lib/src/clixon_hash.c +++ b/lib/src/clixon_hash.c @@ -112,6 +112,7 @@ hash_bucket(const char *str) * * @retval hash Pointer to new hash table. * @retval NULL Error + * @see hash_free For freeing the hash-table */ clicon_hash_t * hash_init(void) diff --git a/lib/src/clixon_string.c b/lib/src/clixon_string.c index ffbc68ff..bf25f146 100644 --- a/lib/src/clixon_string.c +++ b/lib/src/clixon_string.c @@ -552,7 +552,7 @@ clicon_int2str(const map_str2int *mstab, * @param[in] str Input string * @retval int Value * @retval -1 Error, not found - * @note linear search + * @see clicon_str2int_search for optimized lookup, but strings must be sorted */ int clicon_str2int(const map_str2int *mstab, @@ -566,6 +566,65 @@ clicon_str2int(const map_str2int *mstab, return -1; } +/*! Map from string to int using binary (alphatical) search + * @param[in] ms String, integer map + * @param[in] str Input string + * @param[in] low Lower bound index + * @param[in] upper Upper bound index + * @param[in] len Length of array (max) + * @param[out] found Integer found (can also be negative) + * @retval 0 Not found + * @retval 1 Found with "found" value set. + * @note Assumes sorted strings, tree search + */ +static int +str2int_search1(const map_str2int *mstab, + char *str, + int low, + int upper, + int len, + int *found) +{ + const struct map_str2int *ms; + int mid; + int cmp; + + if (upper < low) + return 0; /* not found */ + mid = (low + upper) / 2; + if (mid >= len) /* beyond range */ + return 0; /* not found */ + ms = &mstab[mid]; + if ((cmp = strcmp(str, ms->ms_str)) == 0){ + *found = ms->ms_int; + return 1; /* found */ + } + else if (cmp < 0) + return str2int_search1(mstab, str, low, mid-1, len, found); + else + return str2int_search1(mstab, str, mid+1, upper, len, found); +} + +/*! Map from string to int using str2int map + * @param[in] ms String, integer map + * @param[in] str Input string + * @retval int Value + * @retval -1 Error, not found + * @note Assumes sorted strings, tree search + * @note -1 can not be value + */ +int +clicon_str2int_search(const map_str2int *mstab, + char *str, + int len) +{ + int found; + + if (str2int_search1(mstab, str, 0, len, len, &found)) + return found; + return -1; /* not found */ +} + /*! Split colon-separated node identifier into prefix and name * @param[in] node-id * @param[out] prefix Malloced string. May be NULL. diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index 50f6f7e7..1735df14 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -744,8 +744,10 @@ xml_childvec_get(cxobj *x) * ... * xml_free(x); * @endcode - * @note yspec may be NULL either because it is not known or it is irrelevant, - * eg for body or attribute + * @note As a rule, yspec should be given in normal Clixon calls to enable + * proper sorting and insert functionality. Except as follows: + * - type is body or attribute + * - Yang is unknown * @see xml_sort_insert */ cxobj * @@ -828,6 +830,7 @@ xml_cv_set(cxobj *x, * @retval xmlobj if found. * @retval NULL if no such node found. * @see xml_find_type A more generic function + * @note Linear scalability and relies on strcmp */ cxobj * xml_find(cxobj *x_up, diff --git a/lib/src/clixon_yang_type.c b/lib/src/clixon_yang_type.c index 483ed0ac..1e8f5ff2 100644 --- a/lib/src/clixon_yang_type.c +++ b/lib/src/clixon_yang_type.c @@ -80,19 +80,19 @@ static const map_str2int ytmap[] = { {"int32", CGV_INT32}, /* NOTE, first match on right is significant, dont move */ {"string", CGV_STRING}, /* NOTE, first match on right is significant, dont move */ {"string", CGV_REST}, /* For cv -> yang translation of rest */ - {"binary", CGV_STRING}, - {"bits", CGV_STRING}, + {"binary", CGV_STRING}, + {"bits", CGV_STRING}, {"boolean", CGV_BOOL}, - {"decimal64", CGV_DEC64}, + {"decimal64", CGV_DEC64}, {"empty", CGV_VOID}, /* May not include any content */ - {"enumeration", CGV_STRING}, + {"enumeration", CGV_STRING}, {"identityref", CGV_STRING}, /* XXX */ {"instance-identifier", CGV_STRING}, /* XXX */ - {"int8", CGV_INT8}, - {"int16", CGV_INT16}, + {"int8", CGV_INT8}, + {"int16", CGV_INT16}, {"int64", CGV_INT64}, {"leafref", CGV_STRING}, /* XXX */ - {"uint8", CGV_UINT8}, + {"uint8", CGV_UINT8}, {"uint16", CGV_UINT16}, {"uint32", CGV_UINT32}, {"uint64", CGV_UINT64}, @@ -100,6 +100,33 @@ static const map_str2int ytmap[] = { {NULL, -1} }; +/*! Mapping from yang string types --> cligen types + * @note not 100% same as map_str2int since it has significant order AND + * string->CGV_REST entry removed + */ +static const map_str2int ytmap2[] = { + {"binary", CGV_STRING}, + {"bits", CGV_STRING}, + {"boolean", CGV_BOOL}, + {"decimal64", CGV_DEC64}, + {"empty", CGV_VOID}, /* May not include any content */ + {"enumeration", CGV_STRING}, + {"identityref", CGV_STRING}, /* XXX */ + {"instance-identifier", CGV_STRING}, /* XXX */ + {"int16", CGV_INT16}, + {"int32", CGV_INT32}, + {"int64", CGV_INT64}, + {"int8", CGV_INT8}, + {"leafref", CGV_STRING}, /* XXX */ + {"string", CGV_STRING}, + {"uint16", CGV_UINT16}, + {"uint32", CGV_UINT32}, + {"uint64", CGV_UINT64}, + {"uint8", CGV_UINT8}, + {"union", CGV_REST}, /* Is replaced by actual type */ + {NULL, -1} +}; + /*! Regular expression compiling * @retval -1 Error * @retval 0 regex problem (no match?) @@ -172,7 +199,7 @@ regex_exec(regex_t *re, static int yang_builtin(char *type) { - if (clicon_str2int(ytmap, type) != -1) + if (clicon_str2int_search(ytmap2, type, (sizeof(ytmap)/sizeof(map_str2int))-2) != -1) return 1; return 0; } @@ -318,7 +345,7 @@ yang2cv_type(char *ytype, *cv_type = CGV_ERR; /* built-in types */ - if ((ret = clicon_str2int(ytmap, ytype)) != -1){ + if ((ret = clicon_str2int_search(ytmap2, ytype, (sizeof(ytmap)/sizeof(map_str2int))-2)) != -1){ *cv_type = ret; return 0; } diff --git a/util/clixon_util_xpath.c b/util/clixon_util_xpath.c index b711b072..198cdb8a 100644 --- a/util/clixon_util_xpath.c +++ b/util/clixon_util_xpath.c @@ -33,13 +33,7 @@ See https://www.w3.org/TR/xpath/ - * Turn this on to get an xpath test program - * Usage: xpath [] - * read xpath on first line and xml on rest of lines from input - * Example compile: - gcc -g -o xpath -I. -I../clixon ./clixon_xsl.c -lclixon -lcligen - * Example run: -echo "a\n" | xpath +* */ #ifdef HAVE_CONFIG_H From 7847e74c5e3501cd09da0252f6c057b90f5f899b Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Wed, 24 Apr 2019 09:56:07 +0200 Subject: [PATCH 13/13] yang_cvec_set(y, cvv) added --- CHANGELOG.md | 2 +- lib/clixon/clixon_yang.h | 1 + lib/src/clixon_yang.c | 26 ++++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f929ddb..5231da56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,7 +61,7 @@ * Change all y->ys_keyword to yang_keyword_get(y) * Change all y->ys_argument to yang_argument_get(y) * Change all y->ys_cv to yang_cv_get(y) - * Change all y->ys_cvec to yang_cvec_get(y) + * Change all y->ys_cvec to yang_cvec_get(y) or yang_cvec_set(y, cvv) * xmldb_get() removed unnecessary config option: * Change all calls to dbget from: `xmldb_get(h, db, xpath, 0|1, &xret, msd)` to `xmldb_get(h, db, xpath, &xret, msd)` diff --git a/lib/clixon/clixon_yang.h b/lib/clixon/clixon_yang.h index 81153b72..c577dd4c 100644 --- a/lib/clixon/clixon_yang.h +++ b/lib/clixon/clixon_yang.h @@ -234,6 +234,7 @@ enum rfc_6020 yang_keyword_get(yang_stmt *ys); char *yang_argument_get(yang_stmt *ys); cg_var *yang_cv_get(yang_stmt *ys); cvec *yang_cvec_get(yang_stmt *ys); +int yang_cvec_set(yang_stmt *ys, cvec *cvv); void *yang_regex_cache_get(yang_stmt *ys); int yang_regex_cache_set(yang_stmt *ys, void *regex); diff --git a/lib/src/clixon_yang.c b/lib/src/clixon_yang.c index 0e7d443b..1563b98a 100644 --- a/lib/src/clixon_yang.c +++ b/lib/src/clixon_yang.c @@ -223,12 +223,38 @@ yang_cvec_get(yang_stmt *ys) return ys->ys_cvec; } +/*! Set yang statement CLIgen variable vector + * @param[in] ys Yang statement node + * @param[in] cvec CLIgen vector + * @retval 0 OK + * @retval -1 Error + */ +int +yang_cvec_set(yang_stmt *ys, + cvec *cvv) +{ + if (ys->ys_cvec) + cvec_free(ys->ys_cvec); + ys->ys_cvec = cvv; + return 0; +} + +/*! Get regular expression cache - the compiled regex + * @param[in] ys Yang statement node + * @retval re Compiled regex + * @see regcomp + */ void* yang_regex_cache_get(yang_stmt *ys) { return ys->ys_regex_cache; } +/*! Set regular expression cache - the compiled regex + * @param[in] ys Yang statement node + * @param[in] re Compiled regex + * @see regcomp + */ int yang_regex_cache_set(yang_stmt *ys, void *regex)