From 038fcf1c8b7716415235384de5dc47d07dc45b85 Mon Sep 17 00:00:00 2001 From: Maria Matejka Date: Mon, 27 Sep 2021 17:44:19 +0200 Subject: Locking route attributes cache To access route attribute cache from multiple threads at once, we have to lock the cache on writing. The route attributes data structures are safe to read unless somebody tries to tamper with the cache itself. --- nest/route.h | 10 +++++----- nest/rt-attr.c | 29 +++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 9 deletions(-) (limited to 'nest') diff --git a/nest/route.h b/nest/route.h index a01eff1a..b327e42a 100644 --- a/nest/route.h +++ b/nest/route.h @@ -590,7 +590,7 @@ struct rte_src { typedef struct rta { struct rta *next, **pprev; /* Hash chain */ - u32 uc; /* Use count */ + _Atomic u32 uc; /* Use count */ u32 hash_key; /* Hash over important fields */ struct ea_list *eattrs; /* Extended Attribute chain */ struct hostentry *hostentry; /* Hostentry for recursive next-hops */ @@ -862,15 +862,15 @@ static inline size_t rta_size(const rta *a) { return sizeof(rta) + sizeof(u32)*a #define RTA_MAX_SIZE (sizeof(rta) + sizeof(u32)*MPLS_MAX_LABEL_STACK) rta *rta_lookup(rta *); /* Get rta equivalent to this one, uc++ */ static inline int rta_is_cached(rta *r) { return r->cached; } -static inline rta *rta_clone(rta *r) { r->uc++; return r; } +static inline rta *rta_clone(rta *r) { ASSERT_DIE(0 < atomic_fetch_add_explicit(&r->uc, 1, memory_order_acq_rel)); return r; } void rta__free(rta *r); -static inline void rta_free(rta *r) { if (r && !--r->uc) rta__free(r); } +static inline void rta_free(rta *r) { if (r && (1 == atomic_fetch_sub_explicit(&r->uc, 1, memory_order_acq_rel))) rta__free(r); } rta *rta_do_cow(rta *o, linpool *lp); static inline rta * rta_cow(rta *r, linpool *lp) { return rta_is_cached(r) ? rta_do_cow(r, lp) : r; } static inline void rta_uncache(rta *r) { r->cached = 0; r->uc = 0; } -void rta_dump(rta *); +void rta_dump(const rta *); void rta_dump_all(void); -void rta_show(struct cli *, rta *); +void rta_show(struct cli *, const rta *); u32 rt_get_igp_metric(rte *); struct hostentry * rt_get_hostentry(rtable *tab, ip_addr a, ip_addr ll, rtable *dep); diff --git a/nest/rt-attr.c b/nest/rt-attr.c index f7e33d72..20f9835d 100644 --- a/nest/rt-attr.c +++ b/nest/rt-attr.c @@ -1281,9 +1281,16 @@ rta_lookup(rta *o) ea_normalize(o->eattrs); h = rta_hash(o); + + RTA_LOCK; + for(r=rta_hash_table[h & rta_cache_mask]; r; r=r->next) if (r->hash_key == h && rta_same(r, o)) - return rta_clone(r); + { + atomic_fetch_add_explicit(&r->uc, 1, memory_order_acq_rel); + RTA_UNLOCK; + return r; + } r = rta_copy(o); r->hash_key = h; @@ -1294,12 +1301,21 @@ rta_lookup(rta *o) if (++rta_cache_count > rta_cache_limit) rta_rehash(); + RTA_UNLOCK; return r; } void rta__free(rta *a) { + RTA_LOCK; + if (atomic_load_explicit(&a->uc, memory_order_acquire)) + { + /* Somebody has cloned this rta inbetween. This sometimes happens. */ + RTA_UNLOCK; + return; + } + ASSERT(rta_cache_count && a->cached); rta_cache_count--; *a->pprev = a->next; @@ -1311,6 +1327,7 @@ rta__free(rta *a) ea_free(a->eattrs); a->cached = 0; sl_free(rta_slab(a), a); + RTA_UNLOCK; } rta * @@ -1335,7 +1352,7 @@ rta_do_cow(rta *o, linpool *lp) * This function takes a &rta and dumps its contents to the debug output. */ void -rta_dump(rta *a) +rta_dump(const rta *a) { static char *rts[] = { "", "RTS_STATIC", "RTS_INHERIT", "RTS_DEVICE", "RTS_STAT_DEV", "RTS_REDIR", "RTS_RIP", @@ -1350,7 +1367,7 @@ rta_dump(rta *a) debug(" !CACHED"); debug(" <-%I", a->from); if (a->dest == RTD_UNICAST) - for (struct nexthop *nh = &(a->nh); nh; nh = nh->next) + for (const struct nexthop *nh = &(a->nh); nh; nh = nh->next) { if (ipa_nonzero(nh->gw)) debug(" ->%I", nh->gw); if (nh->labels) debug(" L %d", nh->label[0]); @@ -1377,6 +1394,8 @@ rta_dump_all(void) rta *a; uint h; + RTA_LOCK; + debug("Route attribute cache (%d entries, rehash at %d):\n", rta_cache_count, rta_cache_limit); for(h=0; hnext) @@ -1386,10 +1405,12 @@ rta_dump_all(void) debug("\n"); } debug("\n"); + + RTA_UNLOCK; } void -rta_show(struct cli *c, rta *a) +rta_show(struct cli *c, const rta *a) { cli_printf(c, -1008, "\tType: %s %s", rta_src_names[a->source], ip_scope_text(a->scope)); -- cgit v1.2.3 From 18f66055e3f7eec2108e18679652aee47f5de6b2 Mon Sep 17 00:00:00 2001 From: Maria Matejka Date: Wed, 29 Sep 2021 17:59:50 +0200 Subject: Global table update pool removed --- nest/proto.c | 2 + nest/protocol.h | 3 ++ nest/route.h | 8 ++-- nest/rt-table.c | 116 +++++++++++++++++++++----------------------------- proto/bgp/packets.c | 4 +- proto/static/static.c | 2 +- 6 files changed, 61 insertions(+), 74 deletions(-) (limited to 'nest') diff --git a/nest/proto.c b/nest/proto.c index 930fad1d..f55f347a 100644 --- a/nest/proto.c +++ b/nest/proto.c @@ -245,6 +245,8 @@ proto_add_channel(struct proto *p, struct channel_config *cf) channel_init_limit(c, &c->in_limit, PLD_IN, &cf->in_limit); channel_init_limit(c, &c->out_limit, PLD_OUT, &cf->out_limit); + c->rte_update_pool = lp_new_default(proto_pool); + c->net_type = cf->net_type; c->ra_mode = cf->ra_mode; c->preference = cf->preference; diff --git a/nest/protocol.h b/nest/protocol.h index 440297a1..1647fbba 100644 --- a/nest/protocol.h +++ b/nest/protocol.h @@ -509,6 +509,9 @@ struct channel { u8 limit_actions[PLD_MAX]; /* Limit actions enum */ u8 limit_active; /* Flags for active limits */ + linpool *rte_update_pool; + uint rte_update_nest_cnt; + struct channel_import_stats { /* Import - from protocol to core */ u32 updates_received; /* Number of route updates received */ diff --git a/nest/route.h b/nest/route.h index b327e42a..e2dc9986 100644 --- a/nest/route.h +++ b/nest/route.h @@ -197,6 +197,8 @@ typedef struct rtable { struct fib_iterator nhu_fit; /* Next Hop Update FIB iterator */ struct tbf rl_pipe; /* Rate limiting token buffer for pipe collisions */ + linpool *nhu_lp; /* Linpool used for NHU */ + list subscribers; /* Subscribers for notifications */ struct timer *settle_timer; /* Settle time for notifications */ @@ -874,12 +876,12 @@ void rta_show(struct cli *, const rta *); u32 rt_get_igp_metric(rte *); struct hostentry * rt_get_hostentry(rtable *tab, ip_addr a, ip_addr ll, rtable *dep); -void rta_apply_hostentry(rta *a, struct hostentry *he, mpls_label_stack *mls); +void rta_apply_hostentry(rta *a, struct hostentry *he, mpls_label_stack *mls, linpool *lp); static inline void -rta_set_recursive_next_hop(rtable *dep, rta *a, rtable *tab, ip_addr gw, ip_addr ll, mpls_label_stack *mls) +rta_set_recursive_next_hop(rtable *dep, rta *a, rtable *tab, ip_addr gw, ip_addr ll, mpls_label_stack *mls, linpool *lp) { - rta_apply_hostentry(a, rt_get_hostentry(tab, gw, ll, dep), mls); + rta_apply_hostentry(a, rt_get_hostentry(tab, gw, ll, dep), mls, lp); } /* diff --git a/nest/rt-table.c b/nest/rt-table.c index c67f5bf8..e307449a 100644 --- a/nest/rt-table.c +++ b/nest/rt-table.c @@ -49,8 +49,6 @@ pool *rt_table_pool; -static linpool *rte_update_pool; - list routing_tables; /* Data structures for export journal */ @@ -74,9 +72,6 @@ static void rt_feed_channel(void *); static inline void rt_export_used(rtable *tab); static void rt_export_cleanup(void *tab); -static inline void rte_update_lock(void); -static inline void rte_update_unlock(void); - const char *rt_import_state_name_array[TIS_MAX] = { [TIS_DOWN] = "DOWN", [TIS_UP] = "UP", @@ -112,6 +107,19 @@ const char *rt_export_state_name(u8 state) struct event_cork rt_cork; +static inline void +rte_update_lock(struct channel *c) +{ + c->rte_update_nest_cnt++; +} + +static inline void +rte_update_unlock(struct channel *c) +{ + if (!--c->rte_update_nest_cnt) + lp_flush(c->rte_update_pool); +} + /* Like fib_route(), but skips empty net entries */ static inline void * net_route_ip4(rtable *t, net_addr_ip4 *n) @@ -525,7 +533,7 @@ reject_noset: static inline rte * export_filter(struct channel *c, rte *rt, int silent) { - return export_filter_(c, rt, rte_update_pool, silent); + return export_filter_(c, rt, c->rte_update_pool, silent); } void do_rt_notify_direct(struct channel *c, const net_addr *net, rte *new, const rte *old); @@ -618,6 +626,8 @@ rt_notify_accepted(struct rt_export_request *req, const net_addr *n, struct rt_p { struct channel *c = SKIP_BACK(struct channel, out_req, req); + rte_update_lock(c); + rte nb0, *new_best = NULL; const rte *old_best = NULL; @@ -672,13 +682,12 @@ done: } /* Nothing to export */ - if (!new_best && !old_best) - { + if (new_best || old_best) + do_rt_notify(c, n, new_best, old_best); + else DBG("rt_notify_accepted: nothing to export\n"); - return; - } - do_rt_notify(c, n, new_best, old_best); + rte_update_unlock(c); } @@ -750,6 +759,7 @@ rt_notify_merged(struct rt_export_request *req, const net_addr *n, struct rt_pen { struct channel *c = SKIP_BACK(struct channel, out_req, req); + rte_update_lock(c); // struct proto *p = c->proto; #if 0 /* TODO: Find whether this check is possible when processing multiple changes at once. */ @@ -786,17 +796,19 @@ rt_notify_merged(struct rt_export_request *req, const net_addr *n, struct rt_pen } /* Prepare new merged route */ - rte *new_merged = count ? rt_export_merged(c, feed, count, rte_update_pool, 0) : NULL; + rte *new_merged = count ? rt_export_merged(c, feed, count, c->rte_update_pool, 0) : NULL; if (new_merged || old_best) do_rt_notify(c, n, new_merged, old_best); + + rte_update_unlock(c); } void rt_notify_optimal(struct rt_export_request *req, const net_addr *net, struct rt_pending_export *rpe) { struct channel *c = SKIP_BACK(struct channel, out_req, req); - + rte_update_lock(c); rte *old = RTES_OR_NULL(rpe->old_best); struct rte_storage *new_best = rpe->new_best; @@ -812,13 +824,15 @@ rt_notify_optimal(struct rt_export_request *req, const net_addr *net, struct rt_ rte n0, *new = RTES_CLONE(new_best, &n0); rt_notify_basic(c, net, new, old); } + + rte_update_unlock(c); } void rt_notify_any(struct rt_export_request *req, const net_addr *net, struct rt_pending_export *rpe) { struct channel *c = SKIP_BACK(struct channel, out_req, req); - + rte_update_lock(c); struct rte_src *src = rpe->new ? rpe->new->rte.src : rpe->old->rte.src; rte *old = RTES_OR_NULL(rpe->old); struct rte_storage *new_any = rpe->new; @@ -835,18 +849,23 @@ rt_notify_any(struct rt_export_request *req, const net_addr *net, struct rt_pend rte n0, *new = RTES_CLONE(new_any, &n0); rt_notify_basic(c, net, new, old); } + + rte_update_unlock(c); } void rt_feed_any(struct rt_export_request *req, const net_addr *net, struct rt_pending_export *rpe UNUSED, rte **feed, uint count) { struct channel *c = SKIP_BACK(struct channel, out_req, req); + rte_update_lock(c); for (uint i=0; irpe_next); if (!c->rpe_next) break; - - rte_update_unlock(); } rt_send_export_event(c); @@ -1447,21 +1462,6 @@ rte_recalculate(struct rt_import_hook *c, net *net, rte *new, struct rte_src *sr } -static int rte_update_nest_cnt; /* Nesting counter to allow recursive updates */ - -static inline void -rte_update_lock(void) -{ - rte_update_nest_cnt++; -} - -static inline void -rte_update_unlock(void) -{ - if (!--rte_update_nest_cnt) - lp_flush(rte_update_pool); -} - rte * channel_preimport(struct rt_import_request *req, rte *new, rte *old) { @@ -1534,7 +1534,7 @@ rte_update_direct(struct channel *c, const net_addr *n, rte *new, struct rte_src const struct filter *filter = c->in_filter; struct channel_import_stats *stats = &c->import_stats; - rte_update_lock(); + rte_update_lock(c); if (new) { new->net = n; @@ -1549,7 +1549,7 @@ rte_update_direct(struct channel *c, const net_addr *n, rte *new, struct rte_src new = NULL; } else if ((filter == FILTER_REJECT) || - ((fr = f_run(filter, new, rte_update_pool, 0)) > F_ACCEPT)) + ((fr = f_run(filter, new, c->rte_update_pool, 0)) > F_ACCEPT)) { stats->updates_filtered++; channel_rte_trace_in(D_FILTERS, c, new, "filtered out"); @@ -1565,7 +1565,7 @@ rte_update_direct(struct channel *c, const net_addr *n, rte *new, struct rte_src rte_import(&c->in_req, n, new, src); - rte_update_unlock(); + rte_update_unlock(c); } void @@ -1593,25 +1593,6 @@ rte_import(struct rt_import_request *req, const net_addr *n, rte *new, struct rt rte_recalculate(hook, nn, new, src); } -/* Independent call to rte_announce(), used from next hop - recalculation, outside of rte_update(). new must be non-NULL */ -static inline void -rte_announce_i(rtable *tab, net *net, struct rte_storage *new, struct rte_storage *old, - struct rte_storage *new_best, struct rte_storage *old_best) -{ - rte_update_lock(); - rte_announce(tab, net, new, old, new_best, old_best); - rte_update_unlock(); -} - -static inline void -rte_discard(net *net, rte *old) /* Non-filtered route deletion, used during garbage collection */ -{ - rte_update_lock(); - rte_recalculate(old->sender, net, NULL, old->src); - rte_update_unlock(); -} - /* Check rtable for best route to given net whether it would be exported do p */ int rt_examine(rtable *t, net_addr *a, struct channel *c, const struct filter *filter) @@ -1626,14 +1607,14 @@ rt_examine(rtable *t, net_addr *a, struct channel *c, const struct filter *filte if (!rte_is_valid(&rt)) return 0; - rte_update_lock(); + rte_update_lock(c); /* Rest is stripped down export_filter() */ int v = c->proto->preexport ? c->proto->preexport(c, &rt) : 0; if (v == RIC_PROCESS) - v = (f_run(filter, &rt, rte_update_pool, FF_SILENT) <= F_ACCEPT); + v = (f_run(filter, &rt, c->rte_update_pool, FF_SILENT) <= F_ACCEPT); - rte_update_unlock(); + rte_update_unlock(c); return v > 0; } @@ -2123,6 +2104,8 @@ rt_setup(pool *pp, struct rtable_config *cf) t->rl_pipe = (struct tbf) TBF_DEFAULT_LOG_LIMITS; + t->nhu_lp = lp_new_default(p); + return t; } @@ -2137,7 +2120,6 @@ rt_init(void) { rta_init(); rt_table_pool = rp_new(&root_pool, "Routing tables"); - rte_update_pool = lp_new_default(rt_table_pool); init_list(&routing_tables); ev_init_cork(&rt_cork, "Route Table Cork"); } @@ -2211,7 +2193,7 @@ again: return; } - rte_discard(n, &e->rte); + rte_recalculate(e->rte.sender, n, NULL, e->rte.src); limit--; goto rescan; @@ -2462,7 +2444,7 @@ rta_next_hop_outdated(rta *a) } void -rta_apply_hostentry(rta *a, struct hostentry *he, mpls_label_stack *mls) +rta_apply_hostentry(rta *a, struct hostentry *he, mpls_label_stack *mls, linpool *lp) { a->hostentry = he; a->dest = he->dest; @@ -2497,7 +2479,7 @@ no_nexthop: else { nhr = nhp; - nhp = (nhp ? (nhp->next = lp_alloc(rte_update_pool, NEXTHOP_MAX_SIZE)) : &(a->nh)); + nhp = (nhp ? (nhp->next = lp_alloc(lp, NEXTHOP_MAX_SIZE)) : &(a->nh)); } memset(nhp, 0, NEXTHOP_MAX_SIZE); @@ -2561,7 +2543,7 @@ rt_next_hop_update_rte(rtable *tab, net *n, rte *old) mpls_label_stack mls = { .len = a->nh.labels_orig }; memcpy(mls.stack, &a->nh.label[a->nh.labels - mls.len], mls.len * sizeof(u32)); - rta_apply_hostentry(a, old->attrs->hostentry, &mls); + rta_apply_hostentry(a, old->attrs->hostentry, &mls, tab->nhu_lp); a->cached = 0; rte e0 = *old; @@ -2615,6 +2597,8 @@ rt_next_hop_update_net(rtable *tab, net *n) new->rte.lastmod = current_time(); new->rte.id = hmap_first_zero(&tab->id_map); hmap_set(&tab->id_map, new->rte.id); + + lp_flush(tab->nhu_lp); } ASSERT_DIE(pos == count); @@ -2645,7 +2629,7 @@ rt_next_hop_update_net(rtable *tab, net *n) { "autoupdated [+best]", "autoupdated [best]" } }; rt_rte_trace_in(D_ROUTES, updates[i].new->rte.sender->req, &updates[i].new->rte, best_indicator[nb][ob]); - rte_announce_i(tab, n, updates[i].new, updates[i].old, new, old_best); + rte_announce(tab, n, updates[i].new, updates[i].old, new, old_best); } return count; @@ -2869,21 +2853,17 @@ rt_feed_channel(void *data) uint count = rte_feed_count(n); if (count) { - rte_update_lock(); rte **feed = alloca(count * sizeof(rte *)); rte_feed_obtain(n, feed, count); c->req->export_bulk(c->req, n->n.addr, NULL, feed, count); max_feed -= count; - rte_update_unlock(); } } else if (n->routes && rte_is_valid(&n->routes->rte)) { - rte_update_lock(); struct rt_pending_export rpe = { .new = n->routes, .new_best = n->routes }; c->req->export_one(c->req, n->n.addr, &rpe); max_feed--; - rte_update_unlock(); } for (struct rt_pending_export *rpe = n->first; rpe; rpe = rpe_next(rpe, NULL)) diff --git a/proto/bgp/packets.c b/proto/bgp/packets.c index d2d5b174..ea9adb4c 100644 --- a/proto/bgp/packets.c +++ b/proto/bgp/packets.c @@ -971,7 +971,7 @@ bgp_apply_next_hop(struct bgp_parse_state *s, rta *a, ip_addr gw, ip_addr ll) s->hostentry = rt_get_hostentry(tab, gw, ll, c->c.table); if (!s->mpls) - rta_apply_hostentry(a, s->hostentry, NULL); + rta_apply_hostentry(a, s->hostentry, NULL, s->pool); /* With MPLS, hostentry is applied later in bgp_apply_mpls_labels() */ } @@ -1005,7 +1005,7 @@ bgp_apply_mpls_labels(struct bgp_parse_state *s, rta *a, u32 *labels, uint lnum) ms.len = lnum; memcpy(ms.stack, labels, 4*lnum); - rta_apply_hostentry(a, s->hostentry, &ms); + rta_apply_hostentry(a, s->hostentry, &ms, s->pool); } } diff --git a/proto/static/static.c b/proto/static/static.c index b0c9d640..45791e8e 100644 --- a/proto/static/static.c +++ b/proto/static/static.c @@ -98,7 +98,7 @@ static_announce_rte(struct static_proto *p, struct static_route *r) if (r->dest == RTDX_RECURSIVE) { rtable *tab = ipa_is_ip4(r->via) ? p->igp_table_ip4 : p->igp_table_ip6; - rta_set_recursive_next_hop(p->p.main_channel->table, a, tab, r->via, IPA_NONE, r->mls); + rta_set_recursive_next_hop(p->p.main_channel->table, a, tab, r->via, IPA_NONE, r->mls, static_lp); } /* Already announced */ -- cgit v1.2.3 From c7d0c5b2523a8cbfcaee9a235955dd5e58fab671 Mon Sep 17 00:00:00 2001 From: Maria Matejka Date: Wed, 27 Oct 2021 12:42:05 +0000 Subject: Route subscription uses events --- nest/proto.c | 17 +++++++++-------- nest/route.h | 3 +-- nest/rt-table.c | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) (limited to 'nest') diff --git a/nest/proto.c b/nest/proto.c index f55f347a..35af3c6c 100644 --- a/nest/proto.c +++ b/nest/proto.c @@ -319,9 +319,9 @@ proto_remove_channels(struct proto *p) } static void -channel_roa_in_changed(struct rt_subscription *s) +channel_roa_in_changed(void *_data) { - struct channel *c = s->data; + struct channel *c = _data; CD(c, "Reload triggered by RPKI change"); @@ -329,9 +329,9 @@ channel_roa_in_changed(struct rt_subscription *s) } static void -channel_roa_out_changed(struct rt_subscription *s) +channel_roa_out_changed(void *_data) { - struct channel *c = s->data; + struct channel *c = _data; CD(c, "Feeding triggered by RPKI change"); c->refeed_pending = 1; @@ -349,14 +349,14 @@ struct roa_subscription { static int channel_roa_is_subscribed(struct channel *c, rtable *tab, int dir) { - void (*hook)(struct rt_subscription *) = + void (*hook)(void *) = dir ? channel_roa_in_changed : channel_roa_out_changed; struct roa_subscription *s; node *n; WALK_LIST2(s, n, c->roa_subscriptions, roa_node) - if ((s->s.tab == tab) && (s->s.hook == hook)) + if ((s->s.tab == tab) && (s->s.event->hook == hook)) return 1; return 0; @@ -370,9 +370,9 @@ channel_roa_subscribe(struct channel *c, rtable *tab, int dir) return; struct roa_subscription *s = mb_allocz(c->proto->pool, sizeof(struct roa_subscription)); + s->s.event = ev_new_init(c->proto->pool, dir ? channel_roa_in_changed : channel_roa_out_changed, c); + s->s.event->list = proto_work_list(c->proto); - s->s.hook = dir ? channel_roa_in_changed : channel_roa_out_changed; - s->s.data = c; rt_subscribe(tab, &s->s); add_tail(&c->roa_subscriptions, &s->roa_node); @@ -383,6 +383,7 @@ channel_roa_unsubscribe(struct roa_subscription *s) { rt_unsubscribe(&s->s); rem_node(&s->roa_node); + rfree(s->s.event); mb_free(s); } diff --git a/nest/route.h b/nest/route.h index e2dc9986..683c966e 100644 --- a/nest/route.h +++ b/nest/route.h @@ -213,8 +213,7 @@ typedef struct rtable { struct rt_subscription { node n; rtable *tab; - void (*hook)(struct rt_subscription *b); - void *data; + event *event; }; #define NHU_CLEAN 0 diff --git a/nest/rt-table.c b/nest/rt-table.c index e307449a..fb0496bd 100644 --- a/nest/rt-table.c +++ b/nest/rt-table.c @@ -1979,7 +1979,7 @@ rt_settle_timer(timer *t) struct rt_subscription *s; WALK_LIST(s, tab->subscribers) - s->hook(s); + ev_send(s->event->list, s->event); } static void -- cgit v1.2.3