3737import org .apache .http .conn .DnsResolver ;
3838import org .apache .logging .log4j .LogManager ;
3939import org .apache .logging .log4j .Logger ;
40- import org .apache .lucene .store .AlreadyClosedException ;
4140import org .elasticsearch .ElasticsearchException ;
4241import org .elasticsearch .cluster .coordination .stateless .StoreHeartbeatService ;
42+ import org .elasticsearch .cluster .metadata .ProjectId ;
4343import org .elasticsearch .cluster .metadata .RepositoryMetadata ;
4444import org .elasticsearch .cluster .node .DiscoveryNode ;
45+ import org .elasticsearch .cluster .project .ProjectResolver ;
46+ import org .elasticsearch .cluster .service .ClusterService ;
4547import org .elasticsearch .common .Strings ;
4648import org .elasticsearch .common .component .AbstractLifecycleComponent ;
4749import org .elasticsearch .common .settings .Setting ;
4850import org .elasticsearch .common .settings .Settings ;
49- import org .elasticsearch .common .util .Maps ;
5051import org .elasticsearch .common .util .concurrent .RunOnce ;
52+ import org .elasticsearch .core .FixForMultiProject ;
5153import org .elasticsearch .core .Nullable ;
5254import org .elasticsearch .core .Releasable ;
5355import org .elasticsearch .core .Releasables ;
7274import java .util .function .Consumer ;
7375import java .util .function .Supplier ;
7476
75- import static java .util .Collections .emptyMap ;
7677import static software .amazon .awssdk .core .SdkSystemSetting .AWS_ROLE_ARN ;
7778import static software .amazon .awssdk .core .SdkSystemSetting .AWS_ROLE_SESSION_NAME ;
7879import static software .amazon .awssdk .core .SdkSystemSetting .AWS_WEB_IDENTITY_TOKEN_FILE ;
@@ -93,21 +94,6 @@ class S3Service extends AbstractLifecycleComponent {
9394 TimeValue .timeValueHours (24 ),
9495 Setting .Property .NodeScope
9596 );
96- private volatile Map <S3ClientSettings , AmazonS3Reference > clientsCache = emptyMap ();
97-
98- /**
99- * Client settings calculated from static configuration and settings in the keystore.
100- */
101- private volatile Map <String , S3ClientSettings > staticClientSettings = Map .of (
102- "default" ,
103- S3ClientSettings .getClientSettings (Settings .EMPTY , "default" )
104- );
105-
106- /**
107- * Client settings derived from those in {@link #staticClientSettings} by combining them with settings
108- * in the {@link RepositoryMetadata}.
109- */
110- private volatile Map <Settings , S3ClientSettings > derivedClientSettings = emptyMap ();
11197
11298 private final Runnable defaultRegionSetter ;
11399 private volatile Region defaultRegion ;
@@ -124,13 +110,16 @@ class S3Service extends AbstractLifecycleComponent {
124110 final TimeValue compareAndExchangeTimeToLive ;
125111 final TimeValue compareAndExchangeAntiContentionDelay ;
126112 final boolean isStateless ;
113+ private final S3ClientsManager s3ClientsManager ;
127114
128115 S3Service (
129116 Environment environment ,
130- Settings nodeSettings ,
117+ ClusterService clusterService ,
118+ ProjectResolver projectResolver ,
131119 ResourceWatcherService resourceWatcherService ,
132120 Supplier <Region > defaultRegionSupplier
133121 ) {
122+ final Settings nodeSettings = clusterService .getSettings ();
134123 webIdentityTokenCredentialsProvider = new CustomWebIdentityTokenCredentialsProvider (
135124 environment ,
136125 System ::getenv ,
@@ -142,6 +131,20 @@ class S3Service extends AbstractLifecycleComponent {
142131 compareAndExchangeAntiContentionDelay = REPOSITORY_S3_CAS_ANTI_CONTENTION_DELAY_SETTING .get (nodeSettings );
143132 isStateless = DiscoveryNode .isStateless (nodeSettings );
144133 defaultRegionSetter = new RunOnce (() -> defaultRegion = defaultRegionSupplier .get ());
134+ s3ClientsManager = new S3ClientsManager (
135+ nodeSettings ,
136+ this ::buildClientReference ,
137+ clusterService .threadPool ().generic (),
138+ projectResolver .supportsMultipleProjects ()
139+ );
140+ if (projectResolver .supportsMultipleProjects ()) {
141+ clusterService .addHighPriorityApplier (s3ClientsManager );
142+ }
143+ }
144+
145+ // visible to tests
146+ S3ClientsManager getS3ClientsManager () {
147+ return s3ClientsManager ;
145148 }
146149
147150 /**
@@ -151,86 +154,55 @@ class S3Service extends AbstractLifecycleComponent {
151154 * of being returned to the cache.
152155 */
153156 public synchronized void refreshAndClearCache (Map <String , S3ClientSettings > clientsSettings ) {
154- // shutdown all unused clients
155- // others will shutdown on their respective release
156- releaseCachedClients ();
157- this .staticClientSettings = Maps .ofEntries (clientsSettings .entrySet ());
158- derivedClientSettings = emptyMap ();
159- assert this .staticClientSettings .containsKey ("default" ) : "always at least have 'default'" ;
160- /* clients are built lazily by {@link #client} */
157+ s3ClientsManager .refreshAndClearCacheForClusterClients (clientsSettings );
161158 }
162159
163160 /**
164161 * Attempts to retrieve a client by its repository metadata and settings from the cache.
165162 * If the client does not exist it will be created.
166163 */
164+ @ FixForMultiProject (description = "can be removed once blobstore is project aware" )
167165 public AmazonS3Reference client (RepositoryMetadata repositoryMetadata ) {
168- final S3ClientSettings clientSettings = settings (repositoryMetadata );
169- {
170- final AmazonS3Reference clientReference = clientsCache .get (clientSettings );
171- if (clientReference != null && clientReference .tryIncRef ()) {
172- return clientReference ;
173- }
174- }
175- synchronized (this ) {
176- final AmazonS3Reference existing = clientsCache .get (clientSettings );
177- if (existing != null && existing .tryIncRef ()) {
178- return existing ;
179- }
180-
181- if (lifecycle .started () == false ) {
182- // doClose() calls releaseCachedClients() which is also synchronized (this) so if we're STARTED here then the client we
183- // create will definitely not leak on close.
184- throw new AlreadyClosedException ("S3Service is in state [" + lifecycle + "]" );
185- }
166+ return client (ProjectId .DEFAULT , repositoryMetadata );
167+ }
186168
187- final SdkHttpClient httpClient = buildHttpClient (clientSettings , getCustomDnsResolver ());
188- Releasable toRelease = httpClient ::close ;
189- try {
190- final AmazonS3Reference clientReference = new AmazonS3Reference (buildClient (clientSettings , httpClient ), httpClient );
191- clientReference .mustIncRef ();
192- clientsCache = Maps .copyMapWithAddedEntry (clientsCache , clientSettings , clientReference );
193- toRelease = null ;
194- return clientReference ;
195- } finally {
196- Releasables .close (toRelease );
197- }
198- }
169+ /**
170+ * Attempts to retrieve either a cluster or project client from the client manager. Throws if project-id or
171+ * the client name does not exist. The client maybe initialized lazily.
172+ * @param projectId The project associated with the client, or null if the client is cluster level
173+ */
174+ public AmazonS3Reference client (@ Nullable ProjectId projectId , RepositoryMetadata repositoryMetadata ) {
175+ return s3ClientsManager .client (effectiveProjectId (projectId ), repositoryMetadata );
199176 }
200177
201178 /**
202- * Either fetches {@link S3ClientSettings} for a given {@link RepositoryMetadata} from cached settings or creates them
203- * by overriding static client settings from {@link #staticClientSettings} with settings found in the repository metadata.
204- * @param repositoryMetadata Repository Metadata
205- * @return S3ClientSettings
179+ * We use the default project-id for cluster level clients.
206180 */
207- S3ClientSettings settings (RepositoryMetadata repositoryMetadata ) {
208- final Settings settings = repositoryMetadata .settings ();
209- {
210- final S3ClientSettings existing = derivedClientSettings .get (settings );
211- if (existing != null ) {
212- return existing ;
213- }
214- }
215- final String clientName = S3Repository .CLIENT_NAME .get (settings );
216- final S3ClientSettings staticSettings = staticClientSettings .get (clientName );
217- if (staticSettings != null ) {
218- synchronized (this ) {
219- final S3ClientSettings existing = derivedClientSettings .get (settings );
220- if (existing != null ) {
221- return existing ;
222- }
223- final S3ClientSettings newSettings = staticSettings .refine (settings );
224- derivedClientSettings = Maps .copyMapWithAddedOrReplacedEntry (derivedClientSettings , settings , newSettings );
225- return newSettings ;
226- }
181+ ProjectId effectiveProjectId (@ Nullable ProjectId projectId ) {
182+ return projectId == null ? ProjectId .DEFAULT : projectId ;
183+ }
184+
185+ // TODO: consider moving client building into S3ClientsManager
186+ private AmazonS3Reference buildClientReference (final S3ClientSettings clientSettings ) {
187+ final SdkHttpClient httpClient = buildHttpClient (clientSettings , getCustomDnsResolver ());
188+ Releasable toRelease = httpClient ::close ;
189+ try {
190+ final AmazonS3Reference clientReference = new AmazonS3Reference (buildClient (clientSettings , httpClient ), httpClient );
191+ clientReference .mustIncRef ();
192+ toRelease = null ;
193+ return clientReference ;
194+ } finally {
195+ Releasables .close (toRelease );
227196 }
228- throw new IllegalArgumentException (
229- "Unknown s3 client name ["
230- + clientName
231- + "]. Existing client configs: "
232- + Strings .collectionToDelimitedString (staticClientSettings .keySet (), "," )
233- );
197+ }
198+
199+ @ FixForMultiProject (description = "can be removed once blobstore is project aware" )
200+ S3ClientSettings settings (RepositoryMetadata repositoryMetadata ) {
201+ return settings (ProjectId .DEFAULT , repositoryMetadata );
202+ }
203+
204+ S3ClientSettings settings (@ Nullable ProjectId projectId , RepositoryMetadata repositoryMetadata ) {
205+ return s3ClientsManager .settingsForClient (effectiveProjectId (projectId ), repositoryMetadata );
234206 }
235207
236208 // proxy for testing
@@ -448,18 +420,17 @@ static AwsCredentialsProvider buildCredentials(
448420 }
449421 }
450422
451- private synchronized void releaseCachedClients () {
452- // the clients will shutdown when they will not be used anymore
453- for (final AmazonS3Reference clientReference : clientsCache .values ()) {
454- clientReference .decRef ();
455- }
456- // clear previously cached clients, they will be build lazily
457- clientsCache = emptyMap ();
458- derivedClientSettings = emptyMap ();
423+ @ FixForMultiProject (description = "can be removed once blobstore is project aware" )
424+ public void onBlobStoreClose () {
425+ onBlobStoreClose (ProjectId .DEFAULT );
459426 }
460427
461- public void onBlobStoreClose () {
462- releaseCachedClients ();
428+ /**
429+ * Release clients for the specified project.
430+ * @param projectId The project associated with the client, or null if the client is cluster level
431+ */
432+ public void onBlobStoreClose (@ Nullable ProjectId projectId ) {
433+ s3ClientsManager .releaseCachedClients (effectiveProjectId (projectId ));
463434 }
464435
465436 @ Override
@@ -472,7 +443,7 @@ protected void doStop() {}
472443
473444 @ Override
474445 public void doClose () throws IOException {
475- releaseCachedClients ();
446+ s3ClientsManager . close ();
476447 webIdentityTokenCredentialsProvider .close ();
477448 }
478449
0 commit comments