Blame SOURCES/0077-Ticket-48235-Remove-memberOf-global-lock.patch

6f51e1
From 229f61f5f54aeb9e1a1756f731dfe7bcedbf148c Mon Sep 17 00:00:00 2001
6f51e1
From: Mark Reynolds <mreynolds@redhat.com>
6f51e1
Date: Fri, 13 Oct 2017 07:09:08 -0400
6f51e1
Subject: [PATCH 06/10] Ticket 48235 - Remove memberOf global lock
6f51e1
6f51e1
Bug Description:  The memberOf global lock no longer servers a purpose since
6f51e1
                  the plugin is BETXN.  This was causing potential deadlocks
6f51e1
                  when multiple backends are used.
6f51e1
6f51e1
Fix Description:  Remove the lock, and rework the fixup/ancestors caches/hashtables.
6f51e1
                  Instead of reusing a single cache, we create a fresh cache
6f51e1
                  when we copy the plugin config (which only happens at the start
6f51e1
                  of an operation).  Then we destroy the caches when we free
6f51e1
                  the config.
6f51e1
6f51e1
https://pagure.io/389-ds-base/issue/48235
6f51e1
6f51e1
Reviewed by: tbordaz & firstyear(Thanks!!)
6f51e1
---
6f51e1
 ldap/servers/plugins/memberof/memberof.c        | 312 +++---------------------
6f51e1
 ldap/servers/plugins/memberof/memberof.h        |  17 ++
6f51e1
 ldap/servers/plugins/memberof/memberof_config.c | 152 +++++++++++-
6f51e1
 3 files changed, 200 insertions(+), 281 deletions(-)
6f51e1
6f51e1
diff --git a/ldap/servers/plugins/memberof/memberof.c b/ldap/servers/plugins/memberof/memberof.c
6f51e1
index 9bbe13c9c..bbf47dd49 100644
6f51e1
--- a/ldap/servers/plugins/memberof/memberof.c
6f51e1
+++ b/ldap/servers/plugins/memberof/memberof.c
6f51e1
@@ -49,13 +49,10 @@ static void* _PluginID = NULL;
6f51e1
 static Slapi_DN* _ConfigAreaDN = NULL;
6f51e1
 static Slapi_RWLock *config_rwlock = NULL;
6f51e1
 static Slapi_DN* _pluginDN = NULL;
6f51e1
-static PRMonitor *memberof_operation_lock = 0;
6f51e1
 MemberOfConfig *qsortConfig = 0;
6f51e1
 static int usetxn = 0;
6f51e1
 static int premodfn = 0;
6f51e1
-#define MEMBEROF_HASHTABLE_SIZE 1000
6f51e1
-static PLHashTable *fixup_entry_hashtable = NULL; /* global hash table protected by memberof_lock (memberof_operation_lock) */
6f51e1
-static PLHashTable *group_ancestors_hashtable = NULL; /* global hash table protected by memberof_lock (memberof_operation_lock) */
6f51e1
+
6f51e1
 
6f51e1
 typedef struct _memberofstringll
6f51e1
 {
6f51e1
@@ -73,18 +70,7 @@ typedef struct _memberof_get_groups_data
6f51e1
         PRBool use_cache;
6f51e1
 } memberof_get_groups_data;
6f51e1
 
6f51e1
-/* The key to access the hash table is the normalized DN
6f51e1
- * The normalized DN is stored in the value because:
6f51e1
- *  - It is used in slapi_valueset_find
6f51e1
- *  - It is used to fill the memberof_get_groups_data.group_norm_vals
6f51e1
- */
6f51e1
-typedef struct _memberof_cached_value
6f51e1
-{
6f51e1
-	char *key;
6f51e1
-	char *group_dn_val;
6f51e1
-        char *group_ndn_val;
6f51e1
-	int valid;
6f51e1
-} memberof_cached_value;
6f51e1
+
6f51e1
 struct cache_stat
6f51e1
 {
6f51e1
 	int total_lookup;
6f51e1
@@ -189,14 +175,9 @@ static int memberof_fix_memberof_callback(Slapi_Entry *e, void *callback_data);
6f51e1
 static int memberof_entry_in_scope(MemberOfConfig *config, Slapi_DN *sdn);
6f51e1
 static int memberof_add_objectclass(char *auto_add_oc, const char *dn);
6f51e1
 static int memberof_add_memberof_attr(LDAPMod **mods, const char *dn, char *add_oc);
6f51e1
-static PLHashTable *hashtable_new();
6f51e1
-static void fixup_hashtable_empty(char *msg);
6f51e1
-static PLHashTable *hashtable_new();
6f51e1
-static void ancestor_hashtable_empty(char *msg);
6f51e1
-static void ancestor_hashtable_entry_free(memberof_cached_value *entry);
6f51e1
-static memberof_cached_value *ancestors_cache_lookup(const char *ndn);
6f51e1
-static PRBool ancestors_cache_remove(const char *ndn);
6f51e1
-static PLHashEntry *ancestors_cache_add(const void *key, void *value);
6f51e1
+static memberof_cached_value *ancestors_cache_lookup(MemberOfConfig *config, const char *ndn);
6f51e1
+static PRBool ancestors_cache_remove(MemberOfConfig *config, const char *ndn);
6f51e1
+static PLHashEntry *ancestors_cache_add(MemberOfConfig *config, const void *key, void *value);
6f51e1
 
6f51e1
 /*** implementation ***/
6f51e1
 
6f51e1
@@ -375,12 +356,6 @@ int memberof_postop_start(Slapi_PBlock *pb)
6f51e1
 	slapi_log_err(SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
6f51e1
 		"--> memberof_postop_start\n" );
6f51e1
 
6f51e1
-	memberof_operation_lock = PR_NewMonitor();
6f51e1
-	if(0 == memberof_operation_lock)
6f51e1
-	{
6f51e1
-		rc = -1;
6f51e1
-		goto bail;
6f51e1
-	}
6f51e1
 	if(config_rwlock == NULL){
6f51e1
 		if((config_rwlock = slapi_new_rwlock()) == NULL){
6f51e1
 			rc = -1;
6f51e1
@@ -388,9 +363,6 @@ int memberof_postop_start(Slapi_PBlock *pb)
6f51e1
 		}
6f51e1
 	}
6f51e1
 
6f51e1
-	fixup_entry_hashtable = hashtable_new();
6f51e1
-	group_ancestors_hashtable = hashtable_new();
6f51e1
-
6f51e1
 	/* Set the alternate config area if one is defined. */
6f51e1
 	slapi_pblock_get(pb, SLAPI_PLUGIN_CONFIG_AREA, &config_area);
6f51e1
 	if (config_area)
6f51e1
@@ -482,18 +454,7 @@ int memberof_postop_close(Slapi_PBlock *pb)
6f51e1
 	slapi_sdn_free(&_pluginDN);
6f51e1
 	slapi_destroy_rwlock(config_rwlock);
6f51e1
 	config_rwlock = NULL;
6f51e1
-	PR_DestroyMonitor(memberof_operation_lock);
6f51e1
-	memberof_operation_lock = NULL;
6f51e1
-
6f51e1
-	if (fixup_entry_hashtable) {
6f51e1
-		fixup_hashtable_empty("memberof_postop_close empty fixup_entry_hastable");
6f51e1
-		PL_HashTableDestroy(fixup_entry_hashtable);
6f51e1
-	}
6f51e1
 
6f51e1
-	if (group_ancestors_hashtable) {
6f51e1
-		ancestor_hashtable_empty("memberof_postop_close empty group_ancestors_hashtable");
6f51e1
-		PL_HashTableDestroy(group_ancestors_hashtable);
6f51e1
-	}
6f51e1
 	slapi_log_err(SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
6f51e1
 		     "<-- memberof_postop_close\n" );
6f51e1
 	return 0;
6f51e1
@@ -554,7 +515,7 @@ int memberof_postop_del(Slapi_PBlock *pb)
6f51e1
 {
6f51e1
 	int ret = SLAPI_PLUGIN_SUCCESS;
6f51e1
 	MemberOfConfig *mainConfig = NULL;
6f51e1
-	MemberOfConfig configCopy = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
6f51e1
+	MemberOfConfig configCopy = {0};
6f51e1
 	Slapi_DN *sdn;
6f51e1
 	void *caller_id = NULL;
6f51e1
 
6f51e1
@@ -583,9 +544,6 @@ int memberof_postop_del(Slapi_PBlock *pb)
6f51e1
 		}
6f51e1
 		memberof_copy_config(&configCopy, memberof_get_config());
6f51e1
 		memberof_unlock_config();
6f51e1
-
6f51e1
-		/* get the memberOf operation lock */
6f51e1
-		memberof_lock();
6f51e1
 		
6f51e1
 		/* remove this DN from the
6f51e1
 		 * membership lists of groups
6f51e1
@@ -594,7 +552,6 @@ int memberof_postop_del(Slapi_PBlock *pb)
6f51e1
 			slapi_log_err(SLAPI_LOG_ERR, MEMBEROF_PLUGIN_SUBSYSTEM,
6f51e1
 			                "memberof_postop_del - Error deleting dn (%s) from group. Error (%d)\n",
6f51e1
 			                slapi_sdn_get_dn(sdn),ret);
6f51e1
-			memberof_unlock();
6f51e1
 			goto bail;
6f51e1
 		}
6f51e1
 
6f51e1
@@ -618,7 +575,6 @@ int memberof_postop_del(Slapi_PBlock *pb)
6f51e1
 				}
6f51e1
 			}
6f51e1
 		}
6f51e1
-		memberof_unlock();
6f51e1
 bail:
6f51e1
 		memberof_free_config(&configCopy);
6f51e1
 	}
6f51e1
@@ -813,7 +769,7 @@ memberof_call_foreach_dn(Slapi_PBlock *pb __attribute__((unused)), Slapi_DN *sdn
6f51e1
 		memberof_cached_value *ht_grp = NULL;
6f51e1
 		const char *ndn = slapi_sdn_get_ndn(sdn);
6f51e1
 		
6f51e1
-		ht_grp = ancestors_cache_lookup((const void *) ndn);
6f51e1
+		ht_grp = ancestors_cache_lookup(config, (const void *) ndn);
6f51e1
 		if (ht_grp) {
6f51e1
 #if MEMBEROF_CACHE_DEBUG
6f51e1
 			slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, "memberof_call_foreach_dn: Ancestors of %s already cached (%x)\n", ndn, ht_grp);
6f51e1
@@ -960,7 +916,7 @@ int memberof_postop_modrdn(Slapi_PBlock *pb)
6f51e1
 	if(memberof_oktodo(pb))
6f51e1
 	{
6f51e1
 		MemberOfConfig *mainConfig = 0;
6f51e1
-		MemberOfConfig configCopy = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
6f51e1
+		MemberOfConfig configCopy = {0};
6f51e1
 		struct slapi_entry *pre_e = NULL;
6f51e1
 		struct slapi_entry *post_e = NULL;
6f51e1
 		Slapi_DN *pre_sdn = 0;
6f51e1
@@ -988,8 +944,6 @@ int memberof_postop_modrdn(Slapi_PBlock *pb)
6f51e1
 			goto bail;
6f51e1
 		}
6f51e1
 
6f51e1
-		memberof_lock();
6f51e1
-
6f51e1
 		/*  update any downstream members */
6f51e1
 		if(pre_sdn && post_sdn && configCopy.group_filter &&
6f51e1
 		   0 == slapi_filter_test_simple(post_e, configCopy.group_filter))
6f51e1
@@ -1060,7 +1014,6 @@ int memberof_postop_modrdn(Slapi_PBlock *pb)
6f51e1
 				}
6f51e1
 			}
6f51e1
 		}
6f51e1
-		memberof_unlock();
6f51e1
 bail:
6f51e1
 		memberof_free_config(&configCopy);
6f51e1
 	}
6f51e1
@@ -1220,7 +1173,7 @@ int memberof_postop_modify(Slapi_PBlock *pb)
6f51e1
 	{
6f51e1
 		int config_copied = 0;
6f51e1
 		MemberOfConfig *mainConfig = 0;
6f51e1
-		MemberOfConfig configCopy = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
6f51e1
+		MemberOfConfig configCopy = {0};
6f51e1
 
6f51e1
 		/* get the mod set */
6f51e1
 		slapi_pblock_get(pb, SLAPI_MODIFY_MODS, &mods;;
6f51e1
@@ -1267,8 +1220,6 @@ int memberof_postop_modify(Slapi_PBlock *pb)
6f51e1
 			{
6f51e1
 				int op = slapi_mod_get_operation(smod);
6f51e1
 
6f51e1
-				memberof_lock();
6f51e1
-
6f51e1
 				/* the modify op decides the function */
6f51e1
 				switch(op & ~LDAP_MOD_BVALUES)
6f51e1
 				{
6f51e1
@@ -1280,7 +1231,6 @@ int memberof_postop_modify(Slapi_PBlock *pb)
6f51e1
 								"memberof_postop_modify - Failed to add dn (%s) to target.  "
6f51e1
 								"Error (%d)\n", slapi_sdn_get_dn(sdn), ret );
6f51e1
 							slapi_mod_done(next_mod);
6f51e1
-							memberof_unlock();
6f51e1
 							goto bail;
6f51e1
 						}
6f51e1
 						break;
6f51e1
@@ -1299,7 +1249,6 @@ int memberof_postop_modify(Slapi_PBlock *pb)
6f51e1
 									"memberof_postop_modify - Failed to replace list (%s).  "
6f51e1
 									"Error (%d)\n", slapi_sdn_get_dn(sdn), ret );
6f51e1
 								slapi_mod_done(next_mod);
6f51e1
-								memberof_unlock();
6f51e1
 								goto bail;
6f51e1
 							}
6f51e1
 						}
6f51e1
@@ -1311,7 +1260,6 @@ int memberof_postop_modify(Slapi_PBlock *pb)
6f51e1
 									"memberof_postop_modify: failed to remove dn (%s).  "
6f51e1
 									"Error (%d)\n", slapi_sdn_get_dn(sdn), ret );
6f51e1
 								slapi_mod_done(next_mod);
6f51e1
-								memberof_unlock();
6f51e1
 								goto bail;
6f51e1
 							}
6f51e1
 						}
6f51e1
@@ -1326,7 +1274,6 @@ int memberof_postop_modify(Slapi_PBlock *pb)
6f51e1
 								"memberof_postop_modify - Failed to replace values in  dn (%s).  "
6f51e1
 								"Error (%d)\n", slapi_sdn_get_dn(sdn), ret );
6f51e1
 							slapi_mod_done(next_mod);
6f51e1
-							memberof_unlock();
6f51e1
 							goto bail;
6f51e1
 						}
6f51e1
 						break;
6f51e1
@@ -1342,8 +1289,6 @@ int memberof_postop_modify(Slapi_PBlock *pb)
6f51e1
 						break;
6f51e1
 					}
6f51e1
 				}
6f51e1
-
6f51e1
-				memberof_unlock();
6f51e1
 			}
6f51e1
 
6f51e1
 			slapi_mod_done(next_mod);
6f51e1
@@ -1398,7 +1343,7 @@ int memberof_postop_add(Slapi_PBlock *pb)
6f51e1
 	if(memberof_oktodo(pb) && (sdn = memberof_getsdn(pb)))
6f51e1
 	{
6f51e1
 		struct slapi_entry *e = NULL;
6f51e1
-		MemberOfConfig configCopy = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
6f51e1
+		MemberOfConfig configCopy = {0};
6f51e1
 		MemberOfConfig *mainConfig;
6f51e1
 		slapi_pblock_get( pb, SLAPI_ENTRY_POST_OP, &e );
6f51e1
 
6f51e1
@@ -1424,8 +1369,6 @@ int memberof_postop_add(Slapi_PBlock *pb)
6f51e1
 			int i = 0;
6f51e1
 			Slapi_Attr *attr = 0;
6f51e1
 
6f51e1
-			memberof_lock();
6f51e1
-
6f51e1
 			for (i = 0; configCopy.groupattrs && configCopy.groupattrs[i]; i++)
6f51e1
 			{
6f51e1
 				if(0 == slapi_entry_attr_find(e, configCopy.groupattrs[i], &attr))
6f51e1
@@ -1438,8 +1381,6 @@ int memberof_postop_add(Slapi_PBlock *pb)
6f51e1
 					}
6f51e1
 				}
6f51e1
 			}
6f51e1
-
6f51e1
-			memberof_unlock();
6f51e1
 			memberof_free_config(&configCopy);
6f51e1
 		}
6f51e1
 	}
6f51e1
@@ -2201,7 +2142,7 @@ dump_cache_entry(memberof_cached_value *double_check, const char *msg)
6f51e1
  * the firsts elements of the array has 'valid=1' and the dn/ndn of group it belong to
6f51e1
  */
6f51e1
 static void
6f51e1
-cache_ancestors(Slapi_Value **member_ndn_val, memberof_get_groups_data *groups)
6f51e1
+cache_ancestors(MemberOfConfig *config, Slapi_Value **member_ndn_val, memberof_get_groups_data *groups)
6f51e1
 {
6f51e1
 	Slapi_ValueSet *groupvals = *((memberof_get_groups_data*)groups)->groupvals;
6f51e1
 	Slapi_Value *sval;
6f51e1
@@ -2298,14 +2239,14 @@ cache_ancestors(Slapi_Value **member_ndn_val, memberof_get_groups_data *groups)
6f51e1
 #if MEMBEROF_CACHE_DEBUG
6f51e1
 	dump_cache_entry(cache_entry, key);
6f51e1
 #endif
6f51e1
-	if (ancestors_cache_add((const void*) key_copy, (void *) cache_entry) == NULL) {
6f51e1
+	if (ancestors_cache_add(config, (const void*) key_copy, (void *) cache_entry) == NULL) {
6f51e1
 		slapi_log_err( SLAPI_LOG_FATAL, MEMBEROF_PLUGIN_SUBSYSTEM, "cache_ancestors: Failed to cache ancestor of %s\n", key);
6f51e1
 		ancestor_hashtable_entry_free(cache_entry);
6f51e1
 		slapi_ch_free ((void**)&cache_entry);
6f51e1
 		return;
6f51e1
 	}
6f51e1
 #if MEMBEROF_CACHE_DEBUG
6f51e1
-	if (double_check = ancestors_cache_lookup((const void*) key)) {
6f51e1
+	if (double_check = ancestors_cache_lookup(config, (const void*) key)) {
6f51e1
 		dump_cache_entry(double_check, "read back");
6f51e1
 	}
6f51e1
 #endif
6f51e1
@@ -2390,9 +2331,9 @@ memberof_get_groups_r(MemberOfConfig *config, Slapi_DN *member_sdn,
6f51e1
 		memberof_get_groups_callback, &member_data, &cached, member_data.use_cache);
6f51e1
 
6f51e1
 	merge_ancestors(&member_ndn_val, &member_data, data);
6f51e1
-	if (!cached && member_data.use_cache)
6f51e1
-		cache_ancestors(&member_ndn_val, &member_data);
6f51e1
-
6f51e1
+	if (!cached && member_data.use_cache) {
6f51e1
+		cache_ancestors(config, &member_ndn_val, &member_data);
6f51e1
+	}
6f51e1
 
6f51e1
 	slapi_value_free(&member_ndn_val);
6f51e1
 	slapi_valueset_free(groupvals);
6f51e1
@@ -2969,46 +2910,9 @@ int memberof_qsort_compare(const void *a, const void *b)
6f51e1
 	                                val1, val2);
6f51e1
 }
6f51e1
 
6f51e1
-/* betxn: This locking mechanism is necessary to guarantee the memberof
6f51e1
- * consistency */
6f51e1
-void memberof_lock()
6f51e1
-{
6f51e1
-	if (usetxn) {
6f51e1
-		PR_EnterMonitor(memberof_operation_lock);
6f51e1
-	}
6f51e1
-	if (fixup_entry_hashtable) {
6f51e1
-		fixup_hashtable_empty("memberof_lock");
6f51e1
-	}
6f51e1
-	if (group_ancestors_hashtable) {
6f51e1
-		ancestor_hashtable_empty("memberof_lock empty group_ancestors_hashtable");
6f51e1
-		memset(&cache_stat, 0, sizeof(cache_stat));
6f51e1
-	}
6f51e1
-}
6f51e1
-
6f51e1
-void memberof_unlock()
6f51e1
-{
6f51e1
-	if (group_ancestors_hashtable) {
6f51e1
-		ancestor_hashtable_empty("memberof_unlock empty group_ancestors_hashtable");
6f51e1
-#if MEMBEROF_CACHE_DEBUG
6f51e1
-		slapi_log_err(SLAPI_LOG_FATAL, MEMBEROF_PLUGIN_SUBSYSTEM, "cache statistics: total lookup %d (success %d), add %d, remove %d, enum %d\n",
6f51e1
-			cache_stat.total_lookup, cache_stat.successfull_lookup,
6f51e1
-			cache_stat.total_add, cache_stat.total_remove, cache_stat.total_enumerate);
6f51e1
-		slapi_log_err(SLAPI_LOG_FATAL, MEMBEROF_PLUGIN_SUBSYSTEM, "cache statistics duration: lookup %ld, add %ld, remove %ld, enum %ld\n",
6f51e1
-			cache_stat.cumul_duration_lookup, cache_stat.cumul_duration_add,
6f51e1
-			cache_stat.cumul_duration_remove, cache_stat.cumul_duration_enumerate);
6f51e1
-#endif
6f51e1
-	}
6f51e1
-	if (fixup_entry_hashtable) {
6f51e1
-		fixup_hashtable_empty("memberof_lock");
6f51e1
-	}
6f51e1
-	if (usetxn) {
6f51e1
-		PR_ExitMonitor(memberof_operation_lock);
6f51e1
-	}
6f51e1
-}
6f51e1
-
6f51e1
 void memberof_fixup_task_thread(void *arg)
6f51e1
 {
6f51e1
-	MemberOfConfig configCopy = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
6f51e1
+	MemberOfConfig configCopy = {0};
6f51e1
 	Slapi_Task *task = (Slapi_Task *)arg;
6f51e1
 	task_data *td = NULL;
6f51e1
 	int rc = 0;
6f51e1
@@ -3068,14 +2972,8 @@ void memberof_fixup_task_thread(void *arg)
6f51e1
 		}
6f51e1
 	}
6f51e1
 
6f51e1
-	/* get the memberOf operation lock */
6f51e1
-	memberof_lock();
6f51e1
-
6f51e1
 	/* do real work */
6f51e1
 	rc = memberof_fix_memberof(&configCopy, task, td);
6f51e1
- 
6f51e1
-	/* release the memberOf operation lock */
6f51e1
-	memberof_unlock();
6f51e1
 
6f51e1
 done:
6f51e1
 	if (usetxn && fixup_pb) {
6f51e1
@@ -3240,7 +3138,7 @@ int memberof_fix_memberof(MemberOfConfig *config, Slapi_Task *task, task_data *t
6f51e1
 }
6f51e1
 
6f51e1
 static memberof_cached_value *
6f51e1
-ancestors_cache_lookup(const char *ndn)
6f51e1
+ancestors_cache_lookup(MemberOfConfig *config, const char *ndn)
6f51e1
 {
6f51e1
 	memberof_cached_value *e;
6f51e1
 #if defined(DEBUG) && defined(HAVE_CLOCK_GETTIME)
6f51e1
@@ -3258,7 +3156,7 @@ ancestors_cache_lookup(const char *ndn)
6f51e1
 	}
6f51e1
 #endif
6f51e1
 
6f51e1
-	e = (memberof_cached_value *) PL_HashTableLookupConst(group_ancestors_hashtable, (const void *) ndn);
6f51e1
+	e = (memberof_cached_value *) PL_HashTableLookupConst(config->ancestors_cache, (const void *) ndn);
6f51e1
 
6f51e1
 #if defined(DEBUG) && defined(HAVE_CLOCK_GETTIME)
6f51e1
 	if (start) {
6f51e1
@@ -3274,7 +3172,7 @@ ancestors_cache_lookup(const char *ndn)
6f51e1
 	
6f51e1
 }
6f51e1
 static PRBool
6f51e1
-ancestors_cache_remove(const char *ndn)
6f51e1
+ancestors_cache_remove(MemberOfConfig *config, const char *ndn)
6f51e1
 {
6f51e1
 	PRBool rc;
6f51e1
 #if defined(DEBUG) && defined(HAVE_CLOCK_GETTIME)
6f51e1
@@ -3292,7 +3190,7 @@ ancestors_cache_remove(const char *ndn)
6f51e1
 	}
6f51e1
 #endif
6f51e1
 
6f51e1
-	rc = PL_HashTableRemove(group_ancestors_hashtable, (const void *) ndn);
6f51e1
+	rc = PL_HashTableRemove(config->ancestors_cache, (const void *) ndn);
6f51e1
 
6f51e1
 #if defined(DEBUG) && defined(HAVE_CLOCK_GETTIME)
6f51e1
 	if (start) {
6f51e1
@@ -3305,7 +3203,7 @@ ancestors_cache_remove(const char *ndn)
6f51e1
 }
6f51e1
 
6f51e1
 static PLHashEntry *
6f51e1
-ancestors_cache_add(const void *key, void *value)
6f51e1
+ancestors_cache_add(MemberOfConfig *config, const void *key, void *value)
6f51e1
 {
6f51e1
 	PLHashEntry *e;
6f51e1
 #if defined(DEBUG) && defined(HAVE_CLOCK_GETTIME)
6f51e1
@@ -3322,7 +3220,7 @@ ancestors_cache_add(const void *key, void *value)
6f51e1
 	}
6f51e1
 #endif
6f51e1
 
6f51e1
-	e = PL_HashTableAdd(group_ancestors_hashtable, key, value);
6f51e1
+	e = PL_HashTableAdd(config->ancestors_cache, key, value);
6f51e1
 
6f51e1
 #if defined(DEBUG) && defined(HAVE_CLOCK_GETTIME)
6f51e1
 	if (start) {
6f51e1
@@ -3360,10 +3258,11 @@ int memberof_fix_memberof_callback(Slapi_Entry *e, void *callback_data)
6f51e1
 		goto bail;
6f51e1
 	}
6f51e1
 
6f51e1
-        /* Check if the entry has not already been fixed */
6f51e1
+    /* Check if the entry has not already been fixed */
6f51e1
 	ndn = slapi_sdn_get_ndn(sdn);
6f51e1
-	if (ndn && fixup_entry_hashtable && PL_HashTableLookupConst(fixup_entry_hashtable, (void*) ndn)) {
6f51e1
-		slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, "memberof_fix_memberof_callback: Entry %s already fixed up\n", ndn);
6f51e1
+	if (ndn && config->fixup_cache && PL_HashTableLookupConst(config->fixup_cache, (void*) ndn)) {
6f51e1
+		slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM,
6f51e1
+            "memberof_fix_memberof_callback: Entry %s already fixed up\n", ndn);
6f51e1
 		goto bail;
6f51e1
 	}
6f51e1
 
6f51e1
@@ -3383,9 +3282,9 @@ int memberof_fix_memberof_callback(Slapi_Entry *e, void *callback_data)
6f51e1
 #if MEMBEROF_CACHE_DEBUG
6f51e1
 			slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, "memberof_fix_memberof_callback: This is NOT a group %s\n", ndn);
6f51e1
 #endif
6f51e1
-			ht_grp = ancestors_cache_lookup((const void *) ndn);
6f51e1
+			ht_grp = ancestors_cache_lookup(config, (const void *) ndn);
6f51e1
 			if (ht_grp) {
6f51e1
-				if (ancestors_cache_remove((const void *) ndn)) {
6f51e1
+				if (ancestors_cache_remove(config, (const void *) ndn)) {
6f51e1
 					slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, "memberof_fix_memberof_callback: free cached values for %s\n", ndn);
6f51e1
 					ancestor_hashtable_entry_free(ht_grp);
6f51e1
 					slapi_ch_free((void **) &ht_grp);
6f51e1
@@ -3400,6 +3299,7 @@ int memberof_fix_memberof_callback(Slapi_Entry *e, void *callback_data)
6f51e1
 			}
6f51e1
 		}
6f51e1
 	}
6f51e1
+
6f51e1
 	/* If we found some groups, replace the existing memberOf attribute
6f51e1
 	 * with the found values.  */
6f51e1
 	if (groups && slapi_valueset_count(groups))
6f51e1
@@ -3439,9 +3339,9 @@ int memberof_fix_memberof_callback(Slapi_Entry *e, void *callback_data)
6f51e1
 	slapi_valueset_free(groups);
6f51e1
 
6f51e1
 	/* records that this entry has been fixed up */
6f51e1
-	if (fixup_entry_hashtable) {
6f51e1
+	if (config->fixup_cache) {
6f51e1
 		dn_copy = slapi_ch_strdup(ndn);
6f51e1
-		if (PL_HashTableAdd(fixup_entry_hashtable, dn_copy, dn_copy) == NULL) {
6f51e1
+		if (PL_HashTableAdd(config->fixup_cache, dn_copy, dn_copy) == NULL) {
6f51e1
 			slapi_log_err(SLAPI_LOG_FATAL, MEMBEROF_PLUGIN_SUBSYSTEM, "memberof_fix_memberof_callback: "
6f51e1
 				"failed to add dn (%s) in the fixup hashtable; NSPR error - %d\n",
6f51e1
 				dn_copy, PR_GetError());
6f51e1
@@ -3539,150 +3439,8 @@ memberof_add_objectclass(char *auto_add_oc, const char *dn)
6f51e1
 	return rc;
6f51e1
 }
6f51e1
 
6f51e1
-static PRIntn memberof_hash_compare_keys(const void *v1, const void *v2)
6f51e1
-{
6f51e1
-	PRIntn rc;
6f51e1
-	if (0 == strcasecmp((const char *) v1, (const char *) v2)) {
6f51e1
-		rc = 1;
6f51e1
-	} else {
6f51e1
-		rc = 0;
6f51e1
-	}
6f51e1
-	return rc;
6f51e1
-}
6f51e1
-
6f51e1
-static PRIntn memberof_hash_compare_values(const void *v1, const void *v2)
6f51e1
-{
6f51e1
-	PRIntn rc;
6f51e1
-	if ((char *) v1 == (char *) v2) {
6f51e1
-		rc = 1;
6f51e1
-	} else {
6f51e1
-		rc = 0;
6f51e1
-	}
6f51e1
-	return rc;
6f51e1
-}
6f51e1
-
6f51e1
-/*
6f51e1
- *  Hashing function using Bernstein's method
6f51e1
- */
6f51e1
-static PLHashNumber memberof_hash_fn(const void *key)
6f51e1
-{
6f51e1
-    PLHashNumber hash = 5381;
6f51e1
-    unsigned char *x = (unsigned char *)key;
6f51e1
-    int c;
6f51e1
-
6f51e1
-    while ((c = *x++)){
6f51e1
-        hash = ((hash << 5) + hash) ^ c;
6f51e1
-    }
6f51e1
-    return hash;
6f51e1
-}
6f51e1
-
6f51e1
-/* allocates the plugin hashtable
6f51e1
- * This hash table is used by operation and is protected from
6f51e1
- * concurrent operations with the memberof_lock (if not usetxn, memberof_lock
6f51e1
- * is not implemented and the hash table will be not used.
6f51e1
- *
6f51e1
- * The hash table contains all the DN of the entries for which the memberof
6f51e1
- * attribute has been computed/updated during the current operation
6f51e1
- *
6f51e1
- * hash table should be empty at the beginning and end of the plugin callback
6f51e1
- */
6f51e1
-static PLHashTable *hashtable_new()
6f51e1
-{
6f51e1
-	if (!usetxn) {
6f51e1
-		return NULL;
6f51e1
-	}
6f51e1
-
6f51e1
-	return PL_NewHashTable(MEMBEROF_HASHTABLE_SIZE,
6f51e1
-		memberof_hash_fn,
6f51e1
-		memberof_hash_compare_keys,
6f51e1
-		memberof_hash_compare_values, NULL, NULL);
6f51e1
-}
6f51e1
-/* this function called for each hash node during hash destruction */
6f51e1
-static PRIntn fixup_hashtable_remove(PLHashEntry *he, PRIntn index, void *arg)
6f51e1
-{
6f51e1
-	char *dn_copy;
6f51e1
-
6f51e1
-	if (he == NULL) {
6f51e1
-		return HT_ENUMERATE_NEXT;
6f51e1
-	}
6f51e1
-	dn_copy = (char*) he->value;
6f51e1
-	slapi_ch_free_string(&dn_copy);
6f51e1
-
6f51e1
-	return HT_ENUMERATE_REMOVE;
6f51e1
-}
6f51e1
-
6f51e1
-static void fixup_hashtable_empty(char *msg)
6f51e1
-{
6f51e1
-	if (fixup_entry_hashtable) {
6f51e1
-		PL_HashTableEnumerateEntries(fixup_entry_hashtable, fixup_hashtable_remove, msg);
6f51e1
-	}
6f51e1
-}
6f51e1
-
6f51e1
-
6f51e1
-/* allocates the plugin hashtable
6f51e1
- * This hash table is used by operation and is protected from
6f51e1
- * concurrent operations with the memberof_lock (if not usetxn, memberof_lock
6f51e1
- * is not implemented and the hash table will be not used.
6f51e1
- *
6f51e1
- * The hash table contains all the DN of the entries for which the memberof
6f51e1
- * attribute has been computed/updated during the current operation
6f51e1
- *
6f51e1
- * hash table should be empty at the beginning and end of the plugin callback
6f51e1
- */
6f51e1
-
6f51e1
-static
6f51e1
-void ancestor_hashtable_entry_free(memberof_cached_value *entry)
6f51e1
-{
6f51e1
-	int i;
6f51e1
-	for (i = 0; entry[i].valid; i++) {
6f51e1
-		slapi_ch_free((void **) &entry[i].group_dn_val);
6f51e1
-		slapi_ch_free((void **) &entry[i].group_ndn_val);
6f51e1
-	}
6f51e1
-	/* Here we are at the ending element containing the key */
6f51e1
-	slapi_ch_free((void**) &entry[i].key);
6f51e1
-}
6f51e1
-/* this function called for each hash node during hash destruction */
6f51e1
-static PRIntn ancestor_hashtable_remove(PLHashEntry *he, PRIntn index, void *arg)
6f51e1
+int
6f51e1
+memberof_use_txn()
6f51e1
 {
6f51e1
-	memberof_cached_value *group_ancestor_array;
6f51e1
-
6f51e1
-	if (he == NULL)
6f51e1
-		return HT_ENUMERATE_NEXT;
6f51e1
-
6f51e1
-
6f51e1
-	group_ancestor_array = (memberof_cached_value *) he->value;
6f51e1
-	ancestor_hashtable_entry_free(group_ancestor_array);
6f51e1
-	slapi_ch_free((void **)&group_ancestor_array);
6f51e1
-
6f51e1
-	return HT_ENUMERATE_REMOVE;
6f51e1
+    return usetxn;
6f51e1
 }
6f51e1
-
6f51e1
-static void ancestor_hashtable_empty(char *msg)
6f51e1
-{
6f51e1
-#if defined(DEBUG) && defined(HAVE_CLOCK_GETTIME)
6f51e1
-	long int start;
6f51e1
-	struct timespec tsnow;
6f51e1
-#endif
6f51e1
-
6f51e1
-	if (group_ancestors_hashtable) {
6f51e1
-		cache_stat.total_enumerate++;
6f51e1
-#if defined(DEBUG) && defined(HAVE_CLOCK_GETTIME)
6f51e1
-		if (clock_gettime(CLOCK_REALTIME, &tsnow) != 0) {
6f51e1
-			start = 0;
6f51e1
-		} else {
6f51e1
-			start = tsnow.tv_nsec;
6f51e1
-		}
6f51e1
-#endif
6f51e1
-		PL_HashTableEnumerateEntries(group_ancestors_hashtable, ancestor_hashtable_remove, msg);
6f51e1
-
6f51e1
-#if defined(DEBUG) && defined(HAVE_CLOCK_GETTIME)
6f51e1
-		if (start) {
6f51e1
-			if (clock_gettime(CLOCK_REALTIME, &tsnow) == 0) {
6f51e1
-				cache_stat.cumul_duration_enumerate += (tsnow.tv_nsec - start);
6f51e1
-			}
6f51e1
-		}
6f51e1
-#endif
6f51e1
-	}
6f51e1
-
6f51e1
-}
6f51e1
-
6f51e1
diff --git a/ldap/servers/plugins/memberof/memberof.h b/ldap/servers/plugins/memberof/memberof.h
6f51e1
index 9a3a6a25d..a01c4d247 100644
6f51e1
--- a/ldap/servers/plugins/memberof/memberof.h
6f51e1
+++ b/ldap/servers/plugins/memberof/memberof.h
6f51e1
@@ -62,8 +62,22 @@ typedef struct memberofconfig {
6f51e1
 	int skip_nested;
6f51e1
 	int fixup_task;
6f51e1
 	char *auto_add_oc;
6f51e1
+	PLHashTable *ancestors_cache;
6f51e1
+	PLHashTable *fixup_cache;
6f51e1
 } MemberOfConfig;
6f51e1
 
6f51e1
+/* The key to access the hash table is the normalized DN
6f51e1
+ * The normalized DN is stored in the value because:
6f51e1
+ *  - It is used in slapi_valueset_find
6f51e1
+ *  - It is used to fill the memberof_get_groups_data.group_norm_vals
6f51e1
+ */
6f51e1
+typedef struct _memberof_cached_value
6f51e1
+{
6f51e1
+    char *key;
6f51e1
+    char *group_dn_val;
6f51e1
+    char *group_ndn_val;
6f51e1
+    int valid;
6f51e1
+} memberof_cached_value;
6f51e1
 
6f51e1
 /*
6f51e1
  * functions
6f51e1
@@ -88,5 +102,8 @@ int memberof_apply_config (Slapi_PBlock *pb, Slapi_Entry* entryBefore, Slapi_Ent
6f51e1
 void *memberof_get_plugin_id(void);
6f51e1
 void memberof_release_config(void);
6f51e1
 PRUint64 get_plugin_started(void);
6f51e1
+void ancestor_hashtable_entry_free(memberof_cached_value *entry);
6f51e1
+PLHashTable *hashtable_new();
6f51e1
+int memberof_use_txn();
6f51e1
 
6f51e1
 #endif	/* _MEMBEROF_H_ */
6f51e1
diff --git a/ldap/servers/plugins/memberof/memberof_config.c b/ldap/servers/plugins/memberof/memberof_config.c
6f51e1
index c3474bf2c..3cc7c4d9c 100644
6f51e1
--- a/ldap/servers/plugins/memberof/memberof_config.c
6f51e1
+++ b/ldap/servers/plugins/memberof/memberof_config.c
6f51e1
@@ -14,12 +14,12 @@
6f51e1
  * memberof_config.c - configuration-related code for memberOf plug-in
6f51e1
  *
6f51e1
  */
6f51e1
-
6f51e1
+#include "plhash.h"
6f51e1
 #include <plstr.h>
6f51e1
-
6f51e1
 #include "memberof.h"
6f51e1
 
6f51e1
 #define MEMBEROF_CONFIG_FILTER "(objectclass=*)"
6f51e1
+#define MEMBEROF_HASHTABLE_SIZE 1000
6f51e1
 
6f51e1
 /*
6f51e1
  * The configuration attributes are contained in the plugin entry e.g.
6f51e1
@@ -33,7 +33,9 @@
6f51e1
 
6f51e1
 /*
6f51e1
  * function prototypes
6f51e1
- */ 
6f51e1
+ */
6f51e1
+static void fixup_hashtable_empty( MemberOfConfig *config, char *msg);
6f51e1
+static void ancestor_hashtable_empty(MemberOfConfig *config, char *msg);
6f51e1
 static int memberof_validate_config (Slapi_PBlock *pb, Slapi_Entry* entryBefore, Slapi_Entry* e, 
6f51e1
 										 int *returncode, char *returntext, void *arg);
6f51e1
 static int memberof_search (Slapi_PBlock *pb, Slapi_Entry* entryBefore, Slapi_Entry* e, 
6f51e1
@@ -48,7 +50,7 @@ static int memberof_search (Slapi_PBlock *pb, Slapi_Entry* entryBefore, Slapi_En
6f51e1
 /* This is the main configuration which is updated from dse.ldif.  The
6f51e1
  * config will be copied when it is used by the plug-in to prevent it
6f51e1
  * being changed out from under a running memberOf operation. */
6f51e1
-static MemberOfConfig theConfig = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
6f51e1
+static MemberOfConfig theConfig = {0};
6f51e1
 static Slapi_RWLock *memberof_config_lock = 0;
6f51e1
 static int inited = 0;
6f51e1
 
6f51e1
@@ -696,6 +698,12 @@ memberof_copy_config(MemberOfConfig *dest, MemberOfConfig *src)
6f51e1
 {
6f51e1
 	if (dest && src)
6f51e1
 	{
6f51e1
+        /* Allocate our caches here since we only copy the config at the start of an op */
6f51e1
+        if (memberof_use_txn() == 1){
6f51e1
+            dest->ancestors_cache = hashtable_new();
6f51e1
+            dest->fixup_cache = hashtable_new();
6f51e1
+        }
6f51e1
+
6f51e1
 		/* Check if the copy is already up to date */
6f51e1
 		if (src->groupattrs)
6f51e1
 		{
6f51e1
@@ -799,6 +807,14 @@ memberof_free_config(MemberOfConfig *config)
6f51e1
 		slapi_ch_free_string(&config->memberof_attr);
6f51e1
 		memberof_free_scope(config->entryScopes, &config->entryScopeCount);
6f51e1
 		memberof_free_scope(config->entryScopeExcludeSubtrees, &config->entryExcludeScopeCount);
6f51e1
+		if (config->fixup_cache) {
6f51e1
+			fixup_hashtable_empty(config, "memberof_free_config empty fixup_entry_hastable");
6f51e1
+			PL_HashTableDestroy(config->fixup_cache);
6f51e1
+		}
6f51e1
+		if (config->ancestors_cache) {
6f51e1
+			ancestor_hashtable_empty(config, "memberof_free_config empty group_ancestors_hashtable");
6f51e1
+			PL_HashTableDestroy(config->ancestors_cache);
6f51e1
+		}
6f51e1
 	}
6f51e1
 }
6f51e1
 
6f51e1
@@ -1001,3 +1017,131 @@ bail:
6f51e1
 
6f51e1
 	return ret;
6f51e1
 }
6f51e1
+
6f51e1
+
6f51e1
+static PRIntn memberof_hash_compare_keys(const void *v1, const void *v2)
6f51e1
+{
6f51e1
+	PRIntn rc;
6f51e1
+	if (0 == strcasecmp((const char *) v1, (const char *) v2)) {
6f51e1
+		rc = 1;
6f51e1
+	} else {
6f51e1
+		rc = 0;
6f51e1
+	}
6f51e1
+	return rc;
6f51e1
+}
6f51e1
+
6f51e1
+static PRIntn memberof_hash_compare_values(const void *v1, const void *v2)
6f51e1
+{
6f51e1
+	PRIntn rc;
6f51e1
+	if ((char *) v1 == (char *) v2) {
6f51e1
+		rc = 1;
6f51e1
+	} else {
6f51e1
+		rc = 0;
6f51e1
+	}
6f51e1
+	return rc;
6f51e1
+}
6f51e1
+
6f51e1
+/*
6f51e1
+ *  Hashing function using Bernstein's method
6f51e1
+ */
6f51e1
+static PLHashNumber memberof_hash_fn(const void *key)
6f51e1
+{
6f51e1
+    PLHashNumber hash = 5381;
6f51e1
+    unsigned char *x = (unsigned char *)key;
6f51e1
+    int c;
6f51e1
+
6f51e1
+    while ((c = *x++)){
6f51e1
+        hash = ((hash << 5) + hash) ^ c;
6f51e1
+    }
6f51e1
+    return hash;
6f51e1
+}
6f51e1
+
6f51e1
+/* allocates the plugin hashtable
6f51e1
+ * This hash table is used by operation and is protected from
6f51e1
+ * concurrent operations with the memberof_lock (if not usetxn, memberof_lock
6f51e1
+ * is not implemented and the hash table will be not used.
6f51e1
+ *
6f51e1
+ * The hash table contains all the DN of the entries for which the memberof
6f51e1
+ * attribute has been computed/updated during the current operation
6f51e1
+ *
6f51e1
+ * hash table should be empty at the beginning and end of the plugin callback
6f51e1
+ */
6f51e1
+PLHashTable *hashtable_new(int usetxn)
6f51e1
+{
6f51e1
+	if (!usetxn) {
6f51e1
+		return NULL;
6f51e1
+	}
6f51e1
+
6f51e1
+	return PL_NewHashTable(MEMBEROF_HASHTABLE_SIZE,
6f51e1
+		memberof_hash_fn,
6f51e1
+		memberof_hash_compare_keys,
6f51e1
+		memberof_hash_compare_values, NULL, NULL);
6f51e1
+}
6f51e1
+
6f51e1
+/* this function called for each hash node during hash destruction */
6f51e1
+static PRIntn fixup_hashtable_remove(PLHashEntry *he, PRIntn index __attribute__((unused)), void *arg __attribute__((unused)))
6f51e1
+{
6f51e1
+	char *dn_copy;
6f51e1
+
6f51e1
+	if (he == NULL) {
6f51e1
+		return HT_ENUMERATE_NEXT;
6f51e1
+	}
6f51e1
+	dn_copy = (char*) he->value;
6f51e1
+	slapi_ch_free_string(&dn_copy);
6f51e1
+
6f51e1
+	return HT_ENUMERATE_REMOVE;
6f51e1
+}
6f51e1
+
6f51e1
+static void fixup_hashtable_empty(MemberOfConfig *config, char *msg)
6f51e1
+{
6f51e1
+	if (config->fixup_cache) {
6f51e1
+		PL_HashTableEnumerateEntries(config->fixup_cache, fixup_hashtable_remove, msg);
6f51e1
+	}
6f51e1
+}
6f51e1
+
6f51e1
+
6f51e1
+/* allocates the plugin hashtable
6f51e1
+ * This hash table is used by operation and is protected from
6f51e1
+ * concurrent operations with the memberof_lock (if not usetxn, memberof_lock
6f51e1
+ * is not implemented and the hash table will be not used.
6f51e1
+ *
6f51e1
+ * The hash table contains all the DN of the entries for which the memberof
6f51e1
+ * attribute has been computed/updated during the current operation
6f51e1
+ *
6f51e1
+ * hash table should be empty at the beginning and end of the plugin callback
6f51e1
+ */
6f51e1
+
6f51e1
+void ancestor_hashtable_entry_free(memberof_cached_value *entry)
6f51e1
+{
6f51e1
+	int i;
6f51e1
+
6f51e1
+	for (i = 0; entry[i].valid; i++) {
6f51e1
+		slapi_ch_free((void **) &entry[i].group_dn_val);
6f51e1
+		slapi_ch_free((void **) &entry[i].group_ndn_val);
6f51e1
+	}
6f51e1
+	/* Here we are at the ending element containing the key */
6f51e1
+	slapi_ch_free((void**) &entry[i].key);
6f51e1
+}
6f51e1
+
6f51e1
+/* this function called for each hash node during hash destruction */
6f51e1
+static PRIntn ancestor_hashtable_remove(PLHashEntry *he, PRIntn index __attribute__((unused)), void *arg __attribute__((unused)))
6f51e1
+{
6f51e1
+    memberof_cached_value *group_ancestor_array;
6f51e1
+
6f51e1
+    if (he == NULL) {
6f51e1
+        return HT_ENUMERATE_NEXT;
6f51e1
+    }
6f51e1
+    group_ancestor_array = (memberof_cached_value *) he->value;
6f51e1
+    ancestor_hashtable_entry_free(group_ancestor_array);
6f51e1
+    slapi_ch_free((void **)&group_ancestor_array);
6f51e1
+
6f51e1
+    return HT_ENUMERATE_REMOVE;
6f51e1
+}
6f51e1
+
6f51e1
+static void ancestor_hashtable_empty(MemberOfConfig *config, char *msg)
6f51e1
+{
6f51e1
+	if (config->ancestors_cache) {
6f51e1
+		PL_HashTableEnumerateEntries(config->ancestors_cache, ancestor_hashtable_remove, msg);
6f51e1
+	}
6f51e1
+
6f51e1
+}
6f51e1
-- 
6f51e1
2.13.6
6f51e1