3030import java .util .List ;
3131import java .util .Map ;
3232import java .util .Objects ;
33+ import java .util .concurrent .ConcurrentHashMap ;
3334import java .util .concurrent .Executor ;
3435import java .util .concurrent .atomic .AtomicBoolean ;
3536import java .util .function .Function ;
36- import java .util .function .UnaryOperator ;
3737import java .util .stream .Collectors ;
3838
3939import static java .util .Collections .emptyMap ;
@@ -55,42 +55,45 @@ public class S3ClientsManager implements ClusterStateApplier {
5555 private final Executor executor ;
5656 private final AtomicBoolean managerClosed = new AtomicBoolean (false );
5757 // A map of projectId to clients holder. Adding to and removing from the map happen only in the applier thread.
58- private final Map <ProjectId , ClientsHolder <?>> clientsHolders ;
58+ private final Map <ProjectId , PerProjectClientsHolder > perProjectClientsHolders ;
59+ private final ClusterClientsHolder clusterClientsHolder ;
5960
6061 S3ClientsManager (
6162 Settings nodeSettings ,
6263 Function <S3ClientSettings , AmazonS3Reference > clientBuilder ,
63- UnaryOperator < Map < ProjectId , ClientsHolder <?>>> clientsHoldersWrapper ,
64- Executor executor
64+ Executor executor ,
65+ boolean supportsMultipleProjects
6566 ) {
6667 this .nodeS3Settings = Settings .builder ()
6768 .put (nodeSettings .getByPrefix (S3_SETTING_PREFIX ), false ) // not rely on any cluster scoped secrets
6869 .normalizePrefix (S3_SETTING_PREFIX )
6970 .build ();
7071 this .clientBuilder = clientBuilder ;
7172 this .executor = executor ;
72- this .clientsHolders = clientsHoldersWrapper .apply (Map .of (ProjectId .DEFAULT , new ClusterClientsHolder ()));
73+ this .clusterClientsHolder = new ClusterClientsHolder ();
74+ this .perProjectClientsHolders = supportsMultipleProjects ? new ConcurrentHashMap <>() : null ;
7375 }
7476
7577 @ Override
7678 public void applyClusterState (ClusterChangedEvent event ) {
79+ assert perProjectClientsHolders != null : "expect per-project clients holders to be non-null" ;
7780 final Map <ProjectId , ProjectMetadata > currentProjects = event .state ().metadata ().projects ();
7881
7982 final var updatedPerProjectClients = new HashMap <ProjectId , PerProjectClientsHolder >();
8083 final List <PerProjectClientsHolder > clientsHoldersToClose = new ArrayList <>();
8184 for (var project : currentProjects .values ()) {
82- // Skip the default project, it is handled differently with the ReloadablePlugin interface
85+ // Skip the default project, it is tracked separately with clusterClientsHolder and
86+ // updated differently with the ReloadablePlugin interface
8387 if (ProjectId .DEFAULT .equals (project .id ())) {
8488 continue ;
8589 }
8690 final ProjectSecrets projectSecrets = project .custom (ProjectSecrets .TYPE );
8791 // Project secrets can be null when node restarts. It may not have any s3 credentials if s3 is not in use.
8892 if (projectSecrets == null || projectSecrets .getSettingNames ().stream ().noneMatch (key -> key .startsWith ("s3." ))) {
8993 // Most likely there won't be any existing client, but attempt to remove it anyway just in case
90- final var removed = clientsHolders .remove (project .id ());
94+ final var removed = perProjectClientsHolders .remove (project .id ());
9195 if (removed != null ) {
92- assert removed instanceof PerProjectClientsHolder ;
93- clientsHoldersToClose .add ((PerProjectClientsHolder ) removed );
96+ clientsHoldersToClose .add (removed );
9497 }
9598 continue ;
9699 }
@@ -126,28 +129,27 @@ public void applyClusterState(ClusterChangedEvent event) {
126129
127130 // Updated projects
128131 for (var projectId : updatedPerProjectClients .keySet ()) {
129- final var old = clientsHolders .put (projectId , updatedPerProjectClients .get (projectId ));
132+ assert ProjectId .DEFAULT .equals (projectId ) == false ;
133+ final var old = perProjectClientsHolders .put (projectId , updatedPerProjectClients .get (projectId ));
130134 if (old != null ) {
131- assert old instanceof PerProjectClientsHolder ;
132- clientsHoldersToClose .add ((PerProjectClientsHolder ) old );
135+ clientsHoldersToClose .add (old );
133136 }
134137 }
135- // removed projects
136- for (var projectId : clientsHolders .keySet ()) {
138+ // Removed projects
139+ for (var projectId : perProjectClientsHolders .keySet ()) {
140+ assert ProjectId .DEFAULT .equals (projectId ) == false ;
137141 if (currentProjects .containsKey (projectId ) == false ) {
138- assert ProjectId .DEFAULT .equals (projectId ) == false ;
139- final var removed = clientsHolders .remove (projectId );
140- assert removed instanceof PerProjectClientsHolder ;
141- clientsHoldersToClose .add ((PerProjectClientsHolder ) removed );
142+ final var removed = perProjectClientsHolders .remove (projectId );
143+ clientsHoldersToClose .add (removed );
142144 }
143145 }
144146 // Close stale clients asynchronously without blocking the applier thread
145147 if (clientsHoldersToClose .isEmpty () == false ) {
146- closeClientsAsync (clientsHoldersToClose );
148+ closePerProjectClientsAsync (clientsHoldersToClose );
147149 }
148150 }
149151
150- private void closeClientsAsync (List <PerProjectClientsHolder > clientsHoldersToClose ) {
152+ private void closePerProjectClientsAsync (List <PerProjectClientsHolder > clientsHoldersToClose ) {
151153 executor .execute (new AbstractRunnable () {
152154 @ Override
153155 protected void doRun () throws Exception {
@@ -162,34 +164,45 @@ public void onFailure(Exception e) {
162164 }
163165
164166 // visible for tests
165- Map <ProjectId , ClientsHolder <?>> getClientsHolders () {
166- return Map .copyOf (clientsHolders );
167+ ClusterClientsHolder getClusterClientsHolder () {
168+ return clusterClientsHolder ;
169+ }
170+
171+ // visible for tests
172+ Map <ProjectId , PerProjectClientsHolder > getPerProjectClientsHolders () {
173+ return perProjectClientsHolders == null ? null : Map .copyOf (perProjectClientsHolders );
174+ }
175+
176+ // visible for tests
177+ boolean isManagerClosed () {
178+ return managerClosed .get ();
167179 }
168180
169181 void refreshAndClearCacheForClusterClients (Map <String , S3ClientSettings > clientsSettings ) {
170- final var clientsHolder = clientsHolders .get (ProjectId .DEFAULT );
171- if (clientsHolder instanceof ClusterClientsHolder clusterClientsHolder ) {
172- clusterClientsHolder .refreshAndClearCache (clientsSettings );
173- } else {
174- final String message = "expect cluster clients holder, got " + clientsHolder ;
175- assert false : message ;
176- throw new IllegalStateException (message );
177- }
182+ clusterClientsHolder .refreshAndClearCache (clientsSettings );
178183 }
179184
180185 S3ClientSettings settingsForClient (ProjectId projectId , RepositoryMetadata repositoryMetadata ) {
181- final var clientsHolder = clientsHolders .get (Objects .requireNonNull (projectId ));
186+ if (ProjectId .DEFAULT .equals (Objects .requireNonNull (projectId ))) {
187+ return clusterClientsHolder .singleClientSettings (repositoryMetadata );
188+ }
189+
190+ assert perProjectClientsHolders != null : "expect per-project clients holders to be non-null" ;
191+ final var clientsHolder = perProjectClientsHolders .get (projectId );
182192 if (clientsHolder == null ) {
183- assert ProjectId .DEFAULT .equals (projectId ) == false ;
184193 throw new IllegalArgumentException ("no s3 client is configured for project [" + projectId + "]" );
185194 }
186195 return clientsHolder .singleClientSettings (repositoryMetadata );
187196 }
188197
189198 AmazonS3Reference client (ProjectId projectId , RepositoryMetadata repositoryMetadata ) {
190- final var clientsHolder = clientsHolders .get (Objects .requireNonNull (projectId ));
199+ if (ProjectId .DEFAULT .equals (Objects .requireNonNull (projectId ))) {
200+ return clusterClientsHolder .client (repositoryMetadata );
201+ }
202+
203+ assert perProjectClientsHolders != null : "expect per-project clients holders to be non-null" ;
204+ final var clientsHolder = perProjectClientsHolders .get (projectId );
191205 if (clientsHolder == null ) {
192- assert ProjectId .DEFAULT .equals (projectId ) == false ;
193206 throw new IllegalArgumentException ("no s3 client is configured for project [" + projectId + "]" );
194207 }
195208 return clientsHolder .client (repositoryMetadata );
@@ -200,11 +213,15 @@ AmazonS3Reference client(ProjectId projectId, RepositoryMetadata repositoryMetad
200213 * All clients for the project are closed and will be recreated on next access.
201214 */
202215 void releaseCachedClients (ProjectId projectId ) {
203- final var old = clientsHolders .get (Objects .requireNonNull (projectId ));
216+ if (ProjectId .DEFAULT .equals (Objects .requireNonNull (projectId ))) {
217+ clusterClientsHolder .clearCache ();
218+ return ;
219+ }
220+
221+ assert perProjectClientsHolders != null : "expect per-project clients holders to be non-null" ;
222+ final var old = perProjectClientsHolders .get (projectId );
204223 if (old != null ) {
205224 old .clearCache ();
206- } else {
207- assert ProjectId .DEFAULT .equals (projectId ) == false ;
208225 }
209226 }
210227
@@ -216,12 +233,15 @@ void close() {
216233 // Close all clients holders, they will close their cached clients.
217234 // It's OK if a new clients holder is added concurrently or after this point because
218235 // no new client will be created once the manager is closed, i.e. nothing to release.
219- IOUtils .closeWhileHandlingException (clientsHolders .values ());
236+ if (perProjectClientsHolders != null ) {
237+ IOUtils .closeWhileHandlingException (perProjectClientsHolders .values ());
238+ }
239+ IOUtils .closeWhileHandlingException (clusterClientsHolder );
220240 }
221241 }
222242
223243 private boolean newOrUpdated (ProjectId projectId , Map <String , S3ClientSettings > currentClientSettings ) {
224- final var old = clientsHolders .get (projectId );
244+ final var old = perProjectClientsHolders .get (projectId );
225245 if (old == null ) {
226246 return true ;
227247 }
0 commit comments