Skip to content

Commit 3d62542

Browse files
atmrichkadel
authored andcommitted
HADOOP-9125. LdapGroupsMapping threw CommunicationException after some idle time. Contributed by Kai Zheng.
git-svn-id: https://svn.apache.org/repos/asf/hadoop/common/trunk@1461863 13f79535-47bb-0310-9956-ffa450edef68
1 parent d12be5b commit 3d62542

File tree

3 files changed

+104
-40
lines changed

3 files changed

+104
-40
lines changed

hadoop-common-project/hadoop-common/CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,9 @@ Release 2.0.5-beta - UNRELEASED
598598

599599
HADOOP-9430. TestSSLFactory fails on IBM JVM. (Amir Sanjar via suresh)
600600

601+
HADOOP-9125. LdapGroupsMapping threw CommunicationException after some
602+
idle time. (Kai Zheng via atm)
603+
601604
Release 2.0.4-alpha - UNRELEASED
602605

603606
INCOMPATIBLE CHANGES

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/LdapGroupsMapping.java

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Hashtable;
2525
import java.util.List;
2626

27+
import javax.naming.CommunicationException;
2728
import javax.naming.Context;
2829
import javax.naming.NamingEnumeration;
2930
import javax.naming.NamingException;
@@ -166,6 +167,8 @@ public class LdapGroupsMapping
166167
private String groupMemberAttr;
167168
private String groupNameAttr;
168169

170+
public static int RECONNECT_RETRY_COUNT = 3;
171+
169172
/**
170173
* Returns list of groups for a user.
171174
*
@@ -178,34 +181,63 @@ public class LdapGroupsMapping
178181
*/
179182
@Override
180183
public synchronized List<String> getGroups(String user) throws IOException {
184+
List<String> emptyResults = new ArrayList<String>();
185+
/*
186+
* Normal garbage collection takes care of removing Context instances when they are no longer in use.
187+
* Connections used by Context instances being garbage collected will be closed automatically.
188+
* So in case connection is closed and gets CommunicationException, retry some times with new new DirContext/connection.
189+
*/
190+
try {
191+
return doGetGroups(user);
192+
} catch (CommunicationException e) {
193+
LOG.warn("Connection is closed, will try to reconnect");
194+
} catch (NamingException e) {
195+
LOG.warn("Exception trying to get groups for user " + user, e);
196+
return emptyResults;
197+
}
198+
199+
int retryCount = 0;
200+
while (retryCount ++ < RECONNECT_RETRY_COUNT) {
201+
//reset ctx so that new DirContext can be created with new connection
202+
this.ctx = null;
203+
204+
try {
205+
return doGetGroups(user);
206+
} catch (CommunicationException e) {
207+
LOG.warn("Connection being closed, reconnecting failed, retryCount = " + retryCount);
208+
} catch (NamingException e) {
209+
LOG.warn("Exception trying to get groups for user " + user, e);
210+
return emptyResults;
211+
}
212+
}
213+
214+
return emptyResults;
215+
}
216+
217+
List<String> doGetGroups(String user) throws NamingException {
181218
List<String> groups = new ArrayList<String>();
182219

183-
try {
184-
DirContext ctx = getDirContext();
185-
186-
// Search for the user. We'll only ever need to look at the first result
187-
NamingEnumeration<SearchResult> results = ctx.search(baseDN,
188-
userSearchFilter,
189-
new Object[]{user},
190-
SEARCH_CONTROLS);
191-
if (results.hasMoreElements()) {
192-
SearchResult result = results.nextElement();
193-
String userDn = result.getNameInNamespace();
194-
195-
NamingEnumeration<SearchResult> groupResults =
220+
DirContext ctx = getDirContext();
221+
222+
// Search for the user. We'll only ever need to look at the first result
223+
NamingEnumeration<SearchResult> results = ctx.search(baseDN,
224+
userSearchFilter,
225+
new Object[]{user},
226+
SEARCH_CONTROLS);
227+
if (results.hasMoreElements()) {
228+
SearchResult result = results.nextElement();
229+
String userDn = result.getNameInNamespace();
230+
231+
NamingEnumeration<SearchResult> groupResults =
196232
ctx.search(baseDN,
197-
"(&" + groupSearchFilter + "(" + groupMemberAttr + "={0}))",
198-
new Object[]{userDn},
199-
SEARCH_CONTROLS);
200-
while (groupResults.hasMoreElements()) {
201-
SearchResult groupResult = groupResults.nextElement();
202-
Attribute groupName = groupResult.getAttributes().get(groupNameAttr);
203-
groups.add(groupName.get().toString());
204-
}
233+
"(&" + groupSearchFilter + "(" + groupMemberAttr + "={0}))",
234+
new Object[]{userDn},
235+
SEARCH_CONTROLS);
236+
while (groupResults.hasMoreElements()) {
237+
SearchResult groupResult = groupResults.nextElement();
238+
Attribute groupName = groupResult.getAttributes().get(groupNameAttr);
239+
groups.add(groupName.get().toString());
205240
}
206-
} catch (NamingException e) {
207-
LOG.warn("Exception trying to get groups for user " + user, e);
208-
return new ArrayList<String>();
209241
}
210242

211243
return groups;
@@ -236,7 +268,7 @@ DirContext getDirContext() throws NamingException {
236268

237269
return ctx;
238270
}
239-
271+
240272
/**
241273
* Caches groups, no need to do that for this provider
242274
*/

hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestLdapGroupsMapping.java

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.Arrays;
2727
import java.util.List;
2828

29+
import javax.naming.CommunicationException;
2930
import javax.naming.NamingEnumeration;
3031
import javax.naming.NamingException;
3132
import javax.naming.directory.Attribute;
@@ -46,21 +47,15 @@ public class TestLdapGroupsMapping {
4647
private DirContext mockContext;
4748

4849
private LdapGroupsMapping mappingSpy = spy(new LdapGroupsMapping());
50+
private NamingEnumeration mockUserNamingEnum = mock(NamingEnumeration.class);
51+
private NamingEnumeration mockGroupNamingEnum = mock(NamingEnumeration.class);
52+
private String[] testGroups = new String[] {"group1", "group2"};
4953

5054
@Before
5155
public void setupMocks() throws NamingException {
5256
mockContext = mock(DirContext.class);
5357
doReturn(mockContext).when(mappingSpy).getDirContext();
54-
55-
NamingEnumeration mockUserNamingEnum = mock(NamingEnumeration.class);
56-
NamingEnumeration mockGroupNamingEnum = mock(NamingEnumeration.class);
57-
58-
// The search functionality of the mock context is reused, so we will
59-
// return the user NamingEnumeration first, and then the group
60-
when(mockContext.search(anyString(), anyString(), any(Object[].class),
61-
any(SearchControls.class)))
62-
.thenReturn(mockUserNamingEnum, mockGroupNamingEnum);
63-
58+
6459
SearchResult mockUserResult = mock(SearchResult.class);
6560
// We only ever call hasMoreElements once for the user NamingEnum, so
6661
// we can just have one return value
@@ -76,23 +71,57 @@ public void setupMocks() throws NamingException {
7671

7772
// Define the attribute for the name of the first group
7873
Attribute group1Attr = new BasicAttribute("cn");
79-
group1Attr.add("group1");
74+
group1Attr.add(testGroups[0]);
8075
Attributes group1Attrs = new BasicAttributes();
8176
group1Attrs.put(group1Attr);
8277

8378
// Define the attribute for the name of the second group
8479
Attribute group2Attr = new BasicAttribute("cn");
85-
group2Attr.add("group2");
80+
group2Attr.add(testGroups[1]);
8681
Attributes group2Attrs = new BasicAttributes();
8782
group2Attrs.put(group2Attr);
8883

8984
// This search result gets reused, so return group1, then group2
9085
when(mockGroupResult.getAttributes()).thenReturn(group1Attrs, group2Attrs);
91-
9286
}
9387

9488
@Test
9589
public void testGetGroups() throws IOException, NamingException {
90+
// The search functionality of the mock context is reused, so we will
91+
// return the user NamingEnumeration first, and then the group
92+
when(mockContext.search(anyString(), anyString(), any(Object[].class),
93+
any(SearchControls.class)))
94+
.thenReturn(mockUserNamingEnum, mockGroupNamingEnum);
95+
96+
doTestGetGroups(Arrays.asList(testGroups), 2);
97+
}
98+
99+
@Test
100+
public void testGetGroupsWithConnectionClosed() throws IOException, NamingException {
101+
// The case mocks connection is closed/gc-ed, so the first search call throws CommunicationException,
102+
// then after reconnected return the user NamingEnumeration first, and then the group
103+
when(mockContext.search(anyString(), anyString(), any(Object[].class),
104+
any(SearchControls.class)))
105+
.thenThrow(new CommunicationException("Connection is closed"))
106+
.thenReturn(mockUserNamingEnum, mockGroupNamingEnum);
107+
108+
// Although connection is down but after reconnected it still should retrieve the result groups
109+
doTestGetGroups(Arrays.asList(testGroups), 1 + 2); // 1 is the first failure call
110+
}
111+
112+
@Test
113+
public void testGetGroupsWithLdapDown() throws IOException, NamingException {
114+
// This mocks the case where Ldap server is down, and always throws CommunicationException
115+
when(mockContext.search(anyString(), anyString(), any(Object[].class),
116+
any(SearchControls.class)))
117+
.thenThrow(new CommunicationException("Connection is closed"));
118+
119+
// Ldap server is down, no groups should be retrieved
120+
doTestGetGroups(Arrays.asList(new String[] {}),
121+
1 + LdapGroupsMapping.RECONNECT_RETRY_COUNT); // 1 is the first normal call
122+
}
123+
124+
private void doTestGetGroups(List<String> expectedGroups, int searchTimes) throws IOException, NamingException {
96125
Configuration conf = new Configuration();
97126
// Set this, so we don't throw an exception
98127
conf.set(LdapGroupsMapping.LDAP_URL_KEY, "ldap://test");
@@ -102,10 +131,10 @@ public void testGetGroups() throws IOException, NamingException {
102131
// regardless of input
103132
List<String> groups = mappingSpy.getGroups("some_user");
104133

105-
Assert.assertEquals(Arrays.asList("group1", "group2"), groups);
134+
Assert.assertEquals(expectedGroups, groups);
106135

107136
// We should have searched for a user, and then two groups
108-
verify(mockContext, times(2)).search(anyString(),
137+
verify(mockContext, times(searchTimes)).search(anyString(),
109138
anyString(),
110139
any(Object[].class),
111140
any(SearchControls.class));

0 commit comments

Comments
 (0)