Skip to content

Commit 6ef9e83

Browse files
authored
Convert resource cache to an app service (#2118)
* Remove project from the resource cache * Project utils are now extension methods * Add clear() method for connection settings * Clean up a bunch of tests to make sure they clean up properly
1 parent 5afa080 commit 6ef9e83

File tree

56 files changed

+641
-549
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+641
-549
lines changed

jetbrains-core/it/software/aws/toolkits/jetbrains/services/clouddebug/CloudDebugTestCase.kt

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import software.amazon.awssdk.services.ecs.model.LaunchType
1818
import software.amazon.awssdk.services.ecs.model.Service
1919
import software.aws.toolkits.core.region.AwsRegion
2020
import software.aws.toolkits.core.rules.ECSTemporaryServiceRule
21-
import software.aws.toolkits.jetbrains.core.MockResourceCache
21+
import software.aws.toolkits.jetbrains.core.MockResourceCacheRule
2222
import software.aws.toolkits.jetbrains.core.Resource
2323
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
2424
import software.aws.toolkits.jetbrains.core.credentials.runUnderRealCredentials
@@ -51,6 +51,10 @@ abstract class CloudDebugTestCase(private val taskDefName: String) {
5151
@JvmField
5252
val chain = RuleChain(cfnRule, ecsRule)
5353

54+
@Rule
55+
@JvmField
56+
val resourceCache = MockResourceCacheRule()
57+
5458
@Before
5559
open fun setUp() {
5660
// does not validate that a SSM session is successfully created
@@ -112,17 +116,21 @@ abstract class CloudDebugTestCase(private val taskDefName: String) {
112116
// TODO: delete these horrible mocks once we have a sane implementation...
113117
fun setUpMocks() {
114118
runUnderRealCredentials(getProject()) {
115-
MockResourceCache.getInstance(getProject()).let {
116-
val mockInstrumentedResources = mock<Resource.Cached<Map<String, String>>> {
117-
on { id }.thenReturn("cdb.list_resources")
118-
}
119-
it.addEntry(EcsResources.describeService(instrumentedService.clusterArn(), instrumentedService.serviceArn()), instrumentedService)
120-
it.addEntry(mockInstrumentedResources, mapOf(service.serviceArn() to instrumentationRole))
121-
it.addEntry(
122-
EcsResources.describeTaskDefinition(instrumentedService.taskDefinition()),
123-
ecsClient.describeTaskDefinition { builder -> builder.taskDefinition(instrumentedService.taskDefinition()) }.taskDefinition()
124-
)
119+
val project = getProject()
120+
val mockInstrumentedResources = mock<Resource.Cached<Map<String, String>>> {
121+
on { id }.thenReturn("cdb.list_resources")
125122
}
123+
resourceCache.addEntry(
124+
project,
125+
EcsResources.describeService(instrumentedService.clusterArn(), instrumentedService.serviceArn()),
126+
instrumentedService
127+
)
128+
resourceCache.addEntry(project, mockInstrumentedResources, mapOf(service.serviceArn() to instrumentationRole))
129+
resourceCache.addEntry(
130+
project,
131+
EcsResources.describeTaskDefinition(instrumentedService.taskDefinition()),
132+
ecsClient.describeTaskDefinition { builder -> builder.taskDefinition(instrumentedService.taskDefinition()) }.taskDefinition()
133+
)
126134
}
127135
}
128136

jetbrains-core/resources/META-INF/plugin.xml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,17 +178,19 @@
178178
<applicationService serviceInterface="software.aws.toolkits.jetbrains.core.notification.NoticeManager"
179179
serviceImplementation="software.aws.toolkits.jetbrains.core.notification.DefaultNoticeManager"/>
180180
<applicationService serviceInterface="software.aws.toolkits.core.ToolkitClientManager"
181-
serviceImplementation="software.aws.toolkits.jetbrains.core.AwsClientManager"
182-
testServiceImplementation="software.aws.toolkits.jetbrains.core.MockClientManager"/>
181+
serviceImplementation="software.aws.toolkits.jetbrains.core.AwsClientManager"
182+
testServiceImplementation="software.aws.toolkits.jetbrains.core.MockClientManager"/>
183+
<applicationService serviceInterface="software.aws.toolkits.jetbrains.core.AwsResourceCache"
184+
serviceImplementation="software.aws.toolkits.jetbrains.core.DefaultAwsResourceCache"
185+
testServiceImplementation="software.aws.toolkits.jetbrains.core.MockResourceCache"/>
186+
183187
<projectService serviceImplementation="software.aws.toolkits.jetbrains.core.explorer.ExplorerToolWindow"/>
184-
<projectService serviceInterface="software.aws.toolkits.jetbrains.core.AwsResourceCache"
185-
serviceImplementation="software.aws.toolkits.jetbrains.core.DefaultAwsResourceCache"
186-
testServiceImplementation="software.aws.toolkits.jetbrains.core.MockResourceCache"/>
187188
<projectService serviceImplementation="software.aws.toolkits.jetbrains.services.cloudformation.stack.StackWindowManager"/>
188189
<projectService serviceImplementation="software.aws.toolkits.jetbrains.services.lambda.validation.LambdaHandlerValidator" />
189190
<projectService serviceImplementation="software.aws.toolkits.jetbrains.core.toolwindow.ToolkitToolWindowManager" />
190191
<projectService serviceImplementation="software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogWindow" />
191192
<projectService serviceImplementation="software.aws.toolkits.jetbrains.services.sqs.toolwindow.SqsWindow" />
193+
192194
<toolWindow id="aws.explorer" anchor="left" secondary="true"
193195
factoryClass="software.aws.toolkits.jetbrains.core.explorer.AwsExplorerFactory"
194196
icon="AwsIcons.Logos.AWS"/>

jetbrains-core/src/software/aws/toolkits/jetbrains/core/AwsResourceCache.kt

Lines changed: 86 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package software.aws.toolkits.jetbrains.core
66
import com.intellij.execution.configurations.GeneralCommandLine
77
import com.intellij.openapi.Disposable
88
import com.intellij.openapi.application.ApplicationManager
9-
import com.intellij.openapi.components.ServiceManager
9+
import com.intellij.openapi.components.service
1010
import com.intellij.openapi.project.Project
1111
import com.intellij.util.Alarm
1212
import com.intellij.util.AlarmFactory
@@ -18,6 +18,7 @@ import software.aws.toolkits.core.region.AwsRegion
1818
import software.aws.toolkits.core.utils.getLogger
1919
import software.aws.toolkits.core.utils.warn
2020
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
21+
import software.aws.toolkits.jetbrains.core.credentials.ConnectionSettings
2122
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
2223
import software.aws.toolkits.jetbrains.core.credentials.toEnvironmentVariables
2324
import software.aws.toolkits.jetbrains.core.executables.ExecutableInstance
@@ -34,28 +35,22 @@ import java.util.concurrent.ExecutionException
3435
import java.util.concurrent.TimeUnit
3536
import kotlin.reflect.KClass
3637

38+
// Getting resources can take a long time on a slow connection or if there are a lot of resources. This call should
39+
// always be done in an async context so it should be OK to take multiple seconds.
40+
private val DEFAULT_TIMEOUT = Duration.ofSeconds(30)
41+
3742
/**
3843
* Intended to prevent repeated unnecessary calls to AWS to understand resource state.
3944
*
4045
* Will cache responses from AWS by [AwsRegion]/[ToolkitCredentialsProvider] - generically applicable to any AWS call.
4146
*/
4247
interface AwsResourceCache {
43-
4448
/**
45-
* Get a [resource] either by making a call or returning it from the cache if present and unexpired. Uses the currently [AwsRegion]
46-
* & [ToolkitCredentialsProvider] active in [AwsConnectionManager].
49+
* Get a [resource] either by making a call or returning it from the cache if present and unexpired.
4750
*
4851
* @param[useStale] if an exception occurs attempting to refresh the resource return a cached version if it exists (even if it's expired). Default: true
4952
* @param[forceFetch] force the resource to refresh (and update cache) even if a valid cache version exists. Default: false
5053
*/
51-
fun <T> getResource(resource: Resource<T>, useStale: Boolean = true, forceFetch: Boolean = false): CompletionStage<T>
52-
53-
/**
54-
* @see [getResource]
55-
*
56-
* @param[region] the specific [AwsRegion] to use for this resource
57-
* @param[credentialProvider] the specific [ToolkitCredentialsProvider] to use for this resource
58-
*/
5954
fun <T> getResource(
6055
resource: Resource<T>,
6156
region: AwsRegion,
@@ -65,13 +60,14 @@ interface AwsResourceCache {
6560
): CompletionStage<T>
6661

6762
/**
68-
* Blocking version of [getResource]
69-
*
70-
* @param[useStale] if an exception occurs attempting to refresh the resource return a cached version if it exists (even if it's expired). Default: true
71-
* @param[forceFetch] force the resource to refresh (and update cache) even if a valid cache version exists. Default: false
63+
* @see [getResource]
7264
*/
73-
fun <T> getResourceNow(resource: Resource<T>, timeout: Duration = DEFAULT_TIMEOUT, useStale: Boolean = true, forceFetch: Boolean = false): T =
74-
wait(timeout) { getResource(resource, useStale, forceFetch) }
65+
fun <T> getResource(
66+
resource: Resource<T>,
67+
connectionSettings: ConnectionSettings,
68+
useStale: Boolean = true,
69+
forceFetch: Boolean = false
70+
): CompletionStage<T> = getResource(resource, connectionSettings.region, connectionSettings.credentials, useStale, forceFetch)
7571

7672
/**
7773
* Blocking version of [getResource]
@@ -89,11 +85,15 @@ interface AwsResourceCache {
8985
): T = wait(timeout) { getResource(resource, region, credentialProvider, useStale, forceFetch) }
9086

9187
/**
92-
* Gets the [resource] if it exists in the cache.
93-
*
94-
* @param[useStale] return a cached version if it exists (even if it's expired). Default: true
88+
* Blocking version of [getResource]
9589
*/
96-
fun <T> getResourceIfPresent(resource: Resource<T>, useStale: Boolean = true): T?
90+
fun <T> getResourceNow(
91+
resource: Resource<T>,
92+
connectionSettings: ConnectionSettings,
93+
timeout: Duration = DEFAULT_TIMEOUT,
94+
useStale: Boolean = true,
95+
forceFetch: Boolean = false
96+
): T = getResourceNow(resource, connectionSettings.region, connectionSettings.credentials, timeout, useStale, forceFetch)
9797

9898
/**
9999
* Gets the [resource] if it exists in the cache.
@@ -103,28 +103,30 @@ interface AwsResourceCache {
103103
*/
104104
fun <T> getResourceIfPresent(resource: Resource<T>, region: AwsRegion, credentialProvider: ToolkitCredentialsProvider, useStale: Boolean = true): T?
105105

106+
/**
107+
* Gets the [resource] if it exists in the cache.
108+
*/
109+
fun <T> getResourceIfPresent(resource: Resource<T>, connectionSettings: ConnectionSettings, useStale: Boolean = true): T? =
110+
getResourceIfPresent(resource, connectionSettings.region, connectionSettings.credentials, useStale)
111+
106112
/**
107113
* Clears the contents of the cache across all regions, credentials and resource types.
108114
*/
109115
fun clear()
110116

111117
/**
112-
* Clears the contents of the cache for the specific [resource] type, in the currently active [AwsRegion] & [ToolkitCredentialsProvider]
118+
* Clears the contents of the cache for the specific [ConnectionSettings]
113119
*/
114-
fun clear(resource: Resource<*>)
120+
fun clear(connectionSettings: ConnectionSettings)
115121

116122
/**
117-
* Clears the contents of the cache for the specific [resource] type, [AwsRegion] & [ToolkitCredentialsProvider]
123+
* Clears the contents of the cache for the specific [resource] type] & [ConnectionSettings]
118124
*/
119-
fun clear(resource: Resource<*>, region: AwsRegion, credentialProvider: ToolkitCredentialsProvider)
125+
fun clear(resource: Resource<*>, connectionSettings: ConnectionSettings)
120126

121127
companion object {
122128
@JvmStatic
123-
fun getInstance(project: Project): AwsResourceCache = ServiceManager.getService(project, AwsResourceCache::class.java)
124-
125-
// Getting resources can take a long time on a slow connection or if there are a lot of resources. This call should
126-
// always be done in an async context so it should be OK to take multiple seconds.
127-
private val DEFAULT_TIMEOUT = Duration.ofSeconds(30)
129+
fun getInstance(): AwsResourceCache = service()
128130

129131
private fun <T> wait(timeout: Duration, call: () -> CompletionStage<T>) = try {
130132
call().toCompletableFuture().get(timeout.toMillis(), TimeUnit.MILLISECONDS)
@@ -134,16 +136,55 @@ interface AwsResourceCache {
134136
}
135137
}
136138

137-
fun <T> Project.getResource(resource: Resource<T>, useStale: Boolean = true, forceFetch: Boolean = false) =
138-
AwsResourceCache.getInstance(this).getResource(resource, useStale, forceFetch)
139+
/**
140+
* Get a [resource] either by making a call or returning it from the cache if present and unexpired. Uses the currently [AwsRegion]
141+
* & [ToolkitCredentialsProvider] active in [AwsConnectionManager].
142+
*
143+
* @param[useStale] if an exception occurs attempting to refresh the resource return a cached version if it exists (even if it's expired). Default: true
144+
* @param[forceFetch] force the resource to refresh (and update cache) even if a valid cache version exists. Default: false
145+
*/
146+
fun <T> Project.getResource(resource: Resource<T>, useStale: Boolean = true, forceFetch: Boolean = false): CompletionStage<T> =
147+
AwsResourceCache.getInstance().getResource(resource, this.getConnectionSettings(), useStale, forceFetch)
148+
149+
/**
150+
* Blocking version of [getResource]
151+
*
152+
* @param[useStale] if an exception occurs attempting to refresh the resource return a cached version if it exists (even if it's expired). Default: true
153+
* @param[forceFetch] force the resource to refresh (and update cache) even if a valid cache version exists. Default: false
154+
*/
155+
fun <T> Project.getResourceNow(resource: Resource<T>, timeout: Duration = DEFAULT_TIMEOUT, useStale: Boolean = true, forceFetch: Boolean = false): T =
156+
AwsResourceCache.getInstance().getResourceNow(resource, this.getConnectionSettings(), timeout, useStale, forceFetch)
157+
158+
/**
159+
* Gets the [resource] if it exists in the cache.
160+
*
161+
* @param[useStale] return a cached version if it exists (even if it's expired). Default: true
162+
*/
163+
fun <T> Project.getResourceIfPresent(resource: Resource<T>, useStale: Boolean = true): T? =
164+
AwsResourceCache.getInstance().getResourceIfPresent(resource, this.getConnectionSettings(), useStale)
165+
166+
/**
167+
* Clears the contents of the cache for the specific [resource] type, in the currently active [ConnectionSettings]
168+
*/
169+
fun Project.clearResourceForCurrentConnection(resource: Resource<*>) =
170+
AwsResourceCache.getInstance().clear(resource, this.getConnectionSettings())
171+
172+
/**
173+
* Clears the contents of the cache of all resource types for the currently active [ConnectionSettings]
174+
*/
175+
fun Project.clearResourceForCurrentConnection() =
176+
AwsResourceCache.getInstance().clear(this.getConnectionSettings())
177+
178+
private fun Project.getConnectionSettings(): ConnectionSettings = AwsConnectionManager.getInstance(this).connectionSettings()
179+
?: throw IllegalStateException("Bug: ResourceCache was accessed with invalid ConnectionSettings")
139180

140181
sealed class Resource<T> {
141182

142183
/**
143184
* A [Cached] resource is one whose fetch is potentially expensive, the result of which should be memoized for a period of time ([expiry]).
144185
*/
145186
abstract class Cached<T> : Resource<T>() {
146-
abstract fun fetch(project: Project, region: AwsRegion, credentials: ToolkitCredentialsProvider): T
187+
abstract fun fetch(region: AwsRegion, credentials: ToolkitCredentialsProvider): T
147188
open fun expiry(): Duration = DEFAULT_EXPIRY
148189
abstract val id: String
149190

@@ -178,7 +219,7 @@ class ClientBackedCachedResource<ReturnType, ClientType : SdkClient>(
178219

179220
constructor(sdkClientClass: KClass<ClientType>, id: String, fetchCall: ClientType.() -> ReturnType) : this(sdkClientClass, id, null, fetchCall)
180221

181-
override fun fetch(project: Project, region: AwsRegion, credentials: ToolkitCredentialsProvider): ReturnType {
222+
override fun fetch(region: AwsRegion, credentials: ToolkitCredentialsProvider): ReturnType {
182223
val client = AwsClientManager.getInstance().getClient(sdkClientClass, credentials, region)
183224
return fetchCall(client)
184225
}
@@ -194,7 +235,7 @@ class ExecutableBackedCacheResource<ReturnType, ExecType : ExecutableType<*>>(
194235
private val fetchCall: GeneralCommandLine.() -> ReturnType
195236
) : Resource.Cached<ReturnType>() {
196237

197-
override fun fetch(project: Project, region: AwsRegion, credentials: ToolkitCredentialsProvider): ReturnType {
238+
override fun fetch(region: AwsRegion, credentials: ToolkitCredentialsProvider): ReturnType {
198239
val executableType = ExecutableType.getExecutable(executableTypeClass.java)
199240

200241
val executable = ExecutableManager.getInstance().getExecutableIfPresent(executableType).let {
@@ -217,27 +258,22 @@ class ExecutableBackedCacheResource<ReturnType, ExecType : ExecutableType<*>>(
217258
}
218259

219260
class DefaultAwsResourceCache(
220-
private val project: Project,
221261
private val clock: Clock,
222262
private val maximumCacheEntries: Int,
223263
private val maintenanceInterval: Duration
224264
) : AwsResourceCache, Disposable, ToolkitCredentialsChangeListener {
225265

226266
@Suppress("unused")
227-
constructor(project: Project) : this(project, Clock.systemDefaultZone(), MAXIMUM_CACHE_ENTRIES, DEFAULT_MAINTENANCE_INTERVAL)
267+
constructor() : this(Clock.systemDefaultZone(), MAXIMUM_CACHE_ENTRIES, DEFAULT_MAINTENANCE_INTERVAL)
228268

229269
private val cache = ConcurrentHashMap<CacheKey, Entry<*>>()
230-
private val accountSettings by lazy { AwsConnectionManager.getInstance(project) }
231270
private val alarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, this)
232271

233272
init {
234273
ApplicationManager.getApplication().messageBus.connect(this).subscribe(CredentialManager.CREDENTIALS_CHANGED, this)
235274
scheduleCacheMaintenance()
236275
}
237276

238-
override fun <T> getResource(resource: Resource<T>, useStale: Boolean, forceFetch: Boolean) =
239-
getResource(resource, accountSettings.activeRegion, accountSettings.activeCredentialProvider, useStale, forceFetch)
240-
241277
override fun <T> getResource(
242278
resource: Resource<T>,
243279
region: AwsRegion,
@@ -286,9 +322,6 @@ class DefaultAwsResourceCache(
286322
}
287323
}
288324

289-
override fun <T> getResourceIfPresent(resource: Resource<T>, useStale: Boolean): T? =
290-
getResourceIfPresent(resource, accountSettings.activeRegion, accountSettings.activeCredentialProvider, useStale)
291-
292325
override fun <T> getResourceIfPresent(resource: Resource<T>, region: AwsRegion, credentialProvider: ToolkitCredentialsProvider, useStale: Boolean): T? =
293326
when (resource) {
294327
is Resource.Cached<T> -> {
@@ -301,21 +334,21 @@ class DefaultAwsResourceCache(
301334
is Resource.View<*, T> -> getResourceIfPresent(resource.underlying, region, credentialProvider, useStale)?.let { resource.doMap(it) }
302335
}
303336

304-
override fun clear(resource: Resource<*>) {
305-
clear(resource, accountSettings.activeRegion, accountSettings.activeCredentialProvider)
306-
}
307-
308-
override fun clear(resource: Resource<*>, region: AwsRegion, credentialProvider: ToolkitCredentialsProvider) {
337+
override fun clear(resource: Resource<*>, connectionSettings: ConnectionSettings) {
309338
when (resource) {
310-
is Resource.Cached<*> -> cache.remove(CacheKey(resource.id, region.id, credentialProvider.id))
311-
is Resource.View<*, *> -> clear(resource.underlying, region, credentialProvider)
339+
is Resource.Cached<*> -> cache.remove(CacheKey(resource.id, connectionSettings.region.id, connectionSettings.credentials.id))
340+
is Resource.View<*, *> -> clear(resource.underlying, connectionSettings)
312341
}
313342
}
314343

315344
override fun clear() {
316345
cache.clear()
317346
}
318347

348+
override fun clear(connectionSettings: ConnectionSettings) {
349+
cache.keys.removeIf { it.credentialsId == connectionSettings.credentials.id && it.regionId == connectionSettings.region.id }
350+
}
351+
319352
override fun dispose() {
320353
clear()
321354
}
@@ -343,7 +376,7 @@ class DefaultAwsResourceCache(
343376
}
344377

345378
private fun <T> fetch(context: Context<T>): Entry<T> {
346-
val value = context.resource.fetch(project, context.region, context.credentials)
379+
val value = context.resource.fetch(context.region, context.credentials)
347380
return Entry(clock.instant().plus(context.resource.expiry()), value)
348381
}
349382

0 commit comments

Comments
 (0)