2626import org .elasticsearch .xpack .core .security .action .user .PutUserResponse ;
2727import org .elasticsearch .xpack .security .authz .store .NativePrivilegeStore ;
2828import org .hamcrest .Matchers ;
29+ import org .junit .After ;
2930import org .junit .Before ;
3031
3132import java .util .ArrayList ;
3233import java .util .List ;
3334import java .util .concurrent .CopyOnWriteArrayList ;
3435import java .util .concurrent .CyclicBarrier ;
36+ import java .util .concurrent .ExecutorService ;
37+ import java .util .concurrent .Executors ;
38+ import java .util .concurrent .Future ;
3539import java .util .concurrent .TimeUnit ;
3640import java .util .concurrent .atomic .AtomicInteger ;
3741
3842import static org .elasticsearch .test .hamcrest .ElasticsearchAssertions .assertAcked ;
3943import static org .elasticsearch .xpack .security .support .SecuritySystemIndices .SECURITY_MAIN_ALIAS ;
4044import static org .hamcrest .Matchers .arrayContaining ;
45+ import static org .hamcrest .Matchers .hasItem ;
46+ import static org .hamcrest .Matchers .instanceOf ;
4147import static org .hamcrest .Matchers .is ;
4248import static org .hamcrest .Matchers .not ;
4349import static org .hamcrest .Matchers .notNullValue ;
4450import static org .hamcrest .Matchers .nullValue ;
4551
4652public class SecurityIndexManagerIntegTests extends SecurityIntegTestCase {
4753
54+ private final int concurrentCallsToOnAvailable = 6 ;
55+ private final ExecutorService executor = Executors .newFixedThreadPool (concurrentCallsToOnAvailable );
56+
57+ @ After
58+ public void shutdownExecutor () {
59+ executor .shutdown ();
60+ }
61+
4862 public void testConcurrentOperationsTryingToCreateSecurityIndexAndAlias () throws Exception {
4963 final int processors = Runtime .getRuntime ().availableProcessors ();
5064 final int numThreads = Math .min (50 , scaledRandomIntBetween ((processors + 1 ) / 2 , 4 * processors )); // up to 50 threads
@@ -110,6 +124,12 @@ public void testOnIndexAvailableForSearchIndexCompletesWithinTimeout() throws Ex
110124 // pick longer wait than in the assertBusy that waits for below to ensure index has had enough time to initialize
111125 securityIndexManager .onIndexAvailableForSearch ((ActionListener <Void >) future , TimeValue .timeValueSeconds (40 ));
112126
127+ // check listener added
128+ assertThat (
129+ securityIndexManager .getStateChangeListeners (),
130+ hasItem (instanceOf (SecurityIndexManager .StateConsumerWithCancellable .class ))
131+ );
132+
113133 createSecurityIndexWithWaitForActiveShards ();
114134
115135 assertBusy (
@@ -121,6 +141,12 @@ public void testOnIndexAvailableForSearchIndexCompletesWithinTimeout() throws Ex
121141 // security index creation is complete and index is available for search; therefore whenIndexAvailableForSearch should report
122142 // success in time
123143 future .actionGet ();
144+
145+ // check no remaining listeners
146+ assertThat (
147+ securityIndexManager .getStateChangeListeners (),
148+ not (hasItem (instanceOf (SecurityIndexManager .StateConsumerWithCancellable .class )))
149+ );
124150 }
125151
126152 @ SuppressWarnings ("unchecked" )
@@ -152,6 +178,69 @@ public void testOnIndexAvailableForSearchIndexAlreadyAvailable() throws Exceptio
152178 securityIndexManager .onIndexAvailableForSearch ((ActionListener <Void >) future , TimeValue .timeValueSeconds (10 ));
153179 future .actionGet ();
154180 }
181+
182+ // check no remaining listeners
183+ assertThat (
184+ securityIndexManager .getStateChangeListeners (),
185+ not (hasItem (instanceOf (SecurityIndexManager .StateConsumerWithCancellable .class )))
186+ );
187+ }
188+
189+ @ SuppressWarnings ("unchecked" )
190+ public void testOnIndexAvailableForSearchIndexUnderConcurrentLoad () throws Exception {
191+ final SecurityIndexManager securityIndexManager = internalCluster ().getInstances (NativePrivilegeStore .class )
192+ .iterator ()
193+ .next ()
194+ .getSecurityIndexManager ();
195+ // Long time out calls should all succeed
196+ final List <Future <Void >> futures = new ArrayList <>();
197+ for (int i = 0 ; i < concurrentCallsToOnAvailable / 2 ; i ++) {
198+ final Future <Void > future = executor .submit (() -> {
199+ try {
200+ final ActionFuture <Void > f = new PlainActionFuture <>();
201+ securityIndexManager .onIndexAvailableForSearch ((ActionListener <Void >) f , TimeValue .timeValueSeconds (40 ));
202+ f .actionGet ();
203+ } catch (Exception ex ) {
204+ fail (ex , "should not have encountered exception" );
205+ }
206+ return null ;
207+ });
208+ futures .add (future );
209+ }
210+
211+ // short time-out tasks should all time out
212+ for (int i = 0 ; i < concurrentCallsToOnAvailable / 2 ; i ++) {
213+ final Future <Void > future = executor .submit (() -> {
214+ expectThrows (ElasticsearchTimeoutException .class , () -> {
215+ final ActionFuture <Void > f = new PlainActionFuture <>();
216+ securityIndexManager .onIndexAvailableForSearch ((ActionListener <Void >) f , TimeValue .timeValueMillis (10 ));
217+ f .actionGet ();
218+ });
219+ return null ;
220+ });
221+ futures .add (future );
222+ }
223+
224+ // Sleep a second for short-running calls to timeout
225+ Thread .sleep (1000 );
226+
227+ createSecurityIndexWithWaitForActiveShards ();
228+ // ensure security index manager state is fully in the expected precondition state for this test (ready for search)
229+ assertBusy (
230+ () -> assertThat (securityIndexManager .isAvailable (SecurityIndexManager .Availability .SEARCH_SHARDS ), is (true )),
231+ 30 ,
232+ TimeUnit .SECONDS
233+ );
234+
235+ for (var future : futures ) {
236+ future .get (10 , TimeUnit .SECONDS );
237+ }
238+
239+ // check no remaining listeners
240+ assertThat (
241+ securityIndexManager .getStateChangeListeners (),
242+ not (hasItem (instanceOf (SecurityIndexManager .StateConsumerWithCancellable .class )))
243+ );
155244 }
156245
157246 @ SuppressWarnings ("unchecked" )
@@ -163,9 +252,24 @@ public void testOnIndexAvailableForSearchIndexWaitTimeOut() {
163252 .next ()
164253 .getSecurityIndexManager ();
165254
166- final ActionFuture <Void > future = new PlainActionFuture <>();
167- securityIndexManager .onIndexAvailableForSearch ((ActionListener <Void >) future , TimeValue .timeValueMillis (100 ));
168- expectThrows (ElasticsearchTimeoutException .class , future ::actionGet );
255+ {
256+ final ActionFuture <Void > future = new PlainActionFuture <>();
257+ securityIndexManager .onIndexAvailableForSearch ((ActionListener <Void >) future , TimeValue .timeValueMillis (100 ));
258+ expectThrows (ElasticsearchTimeoutException .class , future ::actionGet );
259+ }
260+
261+ // Also works with 0 timeout
262+ {
263+ final ActionFuture <Void > future = new PlainActionFuture <>();
264+ securityIndexManager .onIndexAvailableForSearch ((ActionListener <Void >) future , TimeValue .timeValueMillis (0 ));
265+ expectThrows (ElasticsearchTimeoutException .class , future ::actionGet );
266+ }
267+
268+ // check no remaining listeners
269+ assertThat (
270+ securityIndexManager .getStateChangeListeners (),
271+ not (hasItem (instanceOf (SecurityIndexManager .StateConsumerWithCancellable .class )))
272+ );
169273 }
170274
171275 public void testSecurityIndexSettingsCannotBeChanged () throws Exception {
0 commit comments