-
Notifications
You must be signed in to change notification settings - Fork 274
Poll for new notifications #5119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
754a269
bca91eb
d894d86
6beeadc
c233764
6f6dd6d
6bf7007
9d32c86
7952d9b
fd2d1fb
cff580f
2d2ffaf
a08194c
17b49fc
c5718f7
691d131
4e588bf
a4427ab
2ce16c3
c54884d
5f140d4
b8b6950
098bb78
ce834c8
50ef8fd
6c76ec4
cc02c33
f04da83
f81d96f
9375226
7add727
35a2b69
aacc8e8
d75d8e9
a14e6c1
cbc2f4d
580674e
fcd46a6
8fba3a3
84ed1ef
26caf31
8546d4a
22420b1
01bb9ef
7097fd9
49924d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| // Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package software.aws.toolkits.jetbrains.core.notifications | ||
|
|
||
| import com.fasterxml.jackson.module.kotlin.readValue | ||
| import com.intellij.openapi.Disposable | ||
| import com.intellij.openapi.application.ApplicationManager | ||
| import com.intellij.openapi.components.Service | ||
| import com.intellij.openapi.util.registry.Registry | ||
| import com.intellij.util.Alarm | ||
| import com.intellij.util.AlarmFactory | ||
| import com.intellij.util.io.HttpRequests | ||
| import kotlinx.coroutines.delay | ||
| import kotlinx.coroutines.runBlocking | ||
| import software.aws.toolkits.core.utils.RemoteResolveParser | ||
| import software.aws.toolkits.core.utils.RemoteResource | ||
| import software.aws.toolkits.core.utils.error | ||
| import software.aws.toolkits.core.utils.getLogger | ||
| import software.aws.toolkits.core.utils.warn | ||
| import software.aws.toolkits.jetbrains.core.DefaultRemoteResourceResolverProvider | ||
| import software.aws.toolkits.jetbrains.core.RemoteResourceResolverProvider | ||
| import software.aws.toolkits.telemetry.Component | ||
| import software.aws.toolkits.telemetry.ToolkitTelemetry | ||
|
Check warning on line 24 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
|
||
Check warningCode scanning / QDJVMC Usage of redundant or deprecated syntax or deprecated symbols Warning
Remove deprecated symbol import
Check warningCode scanning / QDJVMC Usage of redundant or deprecated syntax or deprecated symbols Warning
'ToolkitTelemetry' is deprecated. Use type-safe metric builders
|
||
| import java.io.InputStream | ||
| import java.time.Duration | ||
| import java.util.concurrent.atomic.AtomicBoolean | ||
|
|
||
| private const val MAX_RETRIES = 3 | ||
| private const val RETRY_DELAY_MS = 1000L | ||
|
|
||
| object NotificationFileValidator : RemoteResolveParser { | ||
| override fun canBeParsed(data: InputStream): Boolean = | ||
| try { | ||
| NotificationMapperUtil.mapper.readValue<NotificationsList>(data) | ||
| true | ||
| } catch (e: Exception) { | ||
| false | ||
| } | ||
|
Check warning on line 39 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
|
||
| } | ||
|
|
||
| object NotificationEndpoint { | ||
| fun getEndpoint(): String = | ||
| Registry.get("aws.toolkit.notification.endpoint").asString() | ||
| } | ||
|
|
||
| @Service(Service.Level.APP) | ||
| internal final class NotificationPollingService : Disposable { | ||
|
||
| private val isFirstPoll = AtomicBoolean(true) | ||
| private val observers = mutableListOf<() -> Unit>() | ||
| private val alarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, this) | ||
| private val pollingIntervalMs = Duration.ofMinutes(10).toMillis() | ||
| private val resourceResolver: RemoteResourceResolverProvider = DefaultRemoteResourceResolverProvider() | ||
| private val notificationsResource = object : RemoteResource { | ||
| override val name: String = "notifications.json" | ||
| override val urls: List<String> = listOf(NotificationEndpoint.getEndpoint()) | ||
| override val remoteResolveParser: RemoteResolveParser = NotificationFileValidator | ||
| } | ||
|
|
||
| fun startPolling() { | ||
| val newNotifications = runBlocking { pollForNotifications() } | ||
| if (newNotifications) { | ||
| notifyObservers() | ||
| } | ||
| alarm.addRequest( | ||
| { startPolling() }, | ||
|
Check warning on line 66 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
|
||
| pollingIntervalMs | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Main polling function that checks for updates and downloads if necessary | ||
| * Returns the parsed notifications if successful, null otherwise | ||
| */ | ||
| private suspend fun pollForNotifications(): Boolean { | ||
| var retryCount = 0 | ||
| var lastException: Exception? = null | ||
|
|
||
| while (retryCount < MAX_RETRIES) { | ||
| try { | ||
| val newETag = getNotificationETag() | ||
| if (newETag == NotificationEtagState.getInstance().etag) { | ||
| // for when we need to notify on first poll even when there's no new ETag | ||
| if (isFirstPoll.compareAndSet(true, false)) { | ||
| notifyObservers() | ||
samgst-amazon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| return false | ||
| } | ||
| resourceResolver.get() | ||
| .resolve(notificationsResource) | ||
| .toCompletableFuture() | ||
| .get() | ||
|
Check warning on line 92 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
|
||
Check warningCode scanning / QDJVMC Possibly blocking call in non-blocking context Warning
Possibly blocking call in non-blocking context could lead to thread starvation
|
||
| NotificationEtagState.getInstance().etag = newETag | ||
| return true | ||
| } catch (e: Exception) { | ||
| lastException = e | ||
| LOG.error(e) { "Failed to poll for notifications (attempt ${retryCount + 1}/$MAX_RETRIES)" } | ||
| retryCount++ | ||
|
Check warning on line 98 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
|
||
| if (retryCount < MAX_RETRIES) { | ||
| val backoffDelay = RETRY_DELAY_MS * (1L shl (retryCount - 1)) | ||
| delay(backoffDelay) | ||
|
Check warning on line 101 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
|
||
| } | ||
| } | ||
| } | ||
| emitFailureMetric(lastException) | ||
| return false | ||
|
Check warning on line 106 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
|
||
| } | ||
|
|
||
| private fun getNotificationETag(): String = | ||
| try { | ||
| HttpRequests.request(NotificationEndpoint.getEndpoint()) | ||
| .userAgent("AWS Toolkit for JetBrains") | ||
| .connect { request -> | ||
| request.connection.headerFields["ETag"]?.firstOrNull().orEmpty() | ||
| } | ||
| } catch (e: Exception) { | ||
| LOG.warn { "Failed to fetch notification ETag: $e.message" } | ||
|
Check warning on line 117 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
|
||
| throw e | ||
| } | ||
|
|
||
| private fun emitFailureMetric(e: Exception?) { | ||
| ToolkitTelemetry.showNotification( | ||
|
Check warning on line 122 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
|
||
Check warningCode scanning / QDJVMC Usage of redundant or deprecated syntax or deprecated symbols Warning
'ToolkitTelemetry' is deprecated. Use type-safe metric builders
|
||
| project = null, | ||
| component = Component.Filesystem, | ||
| id = "", | ||
| reason = "Failed to poll for notifications", | ||
| success = false, | ||
|
Check warning on line 127 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
|
||
| reasonDesc = "${e?.javaClass?.simpleName ?: "Unknown"}: ${e?.message ?: "No message"}", | ||
| ) | ||
| } | ||
|
Check warning on line 130 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
|
||
|
|
||
| fun addObserver(observer: () -> Unit) = observers.add(observer) | ||
|
|
||
| private fun notifyObservers() { | ||
| observers.forEach { observer -> | ||
| observer() | ||
| } | ||
| } | ||
|
|
||
| override fun dispose() { | ||
| alarm.dispose() | ||
| } | ||
|
|
||
| companion object { | ||
| private val LOG = getLogger<NotificationPollingService>() | ||
| fun getInstance(): NotificationPollingService = | ||
| ApplicationManager.getApplication().getService(NotificationPollingService::class.java) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| // Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package software.aws.toolkits.jetbrains.core.notifications | ||
|
|
||
| import com.intellij.openapi.application.ApplicationManager | ||
| import com.intellij.openapi.project.Project | ||
| import com.intellij.openapi.startup.ProjectActivity | ||
| import java.util.concurrent.atomic.AtomicBoolean | ||
|
|
||
| internal class NotificationServiceInitializer : ProjectActivity { | ||
|
|
||
| private val initialized = AtomicBoolean(false) | ||
|
|
||
| override suspend fun execute(project: Project) { | ||
| if (ApplicationManager.getApplication().isUnitTestMode) return | ||
| if (initialized.compareAndSet(false, true)) { | ||
| val service = NotificationPollingService.getInstance() | ||
| service.startPolling() | ||
|
Check warning on line 19 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationServiceInitializer.kt
|
||
| } | ||
| } | ||
|
Check warning on line 21 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationServiceInitializer.kt
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| // Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package software.aws.toolkits.jetbrains.core.notifications | ||
|
|
||
| import com.intellij.openapi.components.PersistentStateComponent | ||
| import com.intellij.openapi.components.RoamingType | ||
| import com.intellij.openapi.components.Service | ||
| import com.intellij.openapi.components.State | ||
| import com.intellij.openapi.components.Storage | ||
| import com.intellij.openapi.components.service | ||
|
|
||
| @Service | ||
| @State(name = "notificationDismissals", storages = [Storage("aws.xml", roamingType = RoamingType.DISABLED)]) | ||
| class NotificationDismissalState : PersistentStateComponent<NotificationDismissalConfiguration> { | ||
| private val state = NotificationDismissalConfiguration() | ||
|
|
||
| override fun getState(): NotificationDismissalConfiguration = state | ||
|
Check warning on line 18 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationStateUtils.kt
|
||
|
|
||
| override fun loadState(state: NotificationDismissalConfiguration) { | ||
| this.state.dismissedNotificationIds.clear() | ||
| this.state.dismissedNotificationIds.addAll(state.dismissedNotificationIds) | ||
| } | ||
|
Check warning on line 23 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationStateUtils.kt
|
||
|
|
||
| fun isDismissed(notificationId: String): Boolean = | ||
| state.dismissedNotificationIds.contains(notificationId) | ||
|
|
||
| fun dismissNotification(notificationId: String) { | ||
| state.dismissedNotificationIds.add(notificationId) | ||
| } | ||
|
|
||
| companion object { | ||
| fun getInstance(): NotificationDismissalState = | ||
| service() | ||
|
Check warning on line 34 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationStateUtils.kt
|
||
| } | ||
| } | ||
|
|
||
| data class NotificationDismissalConfiguration( | ||
| var dismissedNotificationIds: MutableSet<String> = mutableSetOf(), | ||
| ) | ||
|
|
||
| @Service | ||
| @State(name = "notificationEtag", storages = [Storage("aws.xml", roamingType = RoamingType.DISABLED)]) | ||
| class NotificationEtagState : PersistentStateComponent<NotificationEtagConfiguration> { | ||
| private val state = NotificationEtagConfiguration() | ||
|
|
||
| override fun getState(): NotificationEtagConfiguration = state | ||
|
Check warning on line 47 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationStateUtils.kt
|
||
|
|
||
| override fun loadState(state: NotificationEtagConfiguration) { | ||
| this.state.etag = state.etag | ||
| } | ||
|
Check warning on line 51 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationStateUtils.kt
|
||
|
|
||
| var etag: String? | ||
| get() = state.etag | ||
| set(value) { | ||
| state.etag = value | ||
| } | ||
|
|
||
| companion object { | ||
| fun getInstance(): NotificationEtagState = | ||
| service() | ||
| } | ||
| } | ||
|
|
||
| data class NotificationEtagConfiguration( | ||
| var etag: String? = null, | ||
| ) | ||
|
|
||
| @Service | ||
| class BannerNotificationService { | ||
| private val notifications = mutableMapOf<String, BannerContent>() | ||
|
Check warning on line 71 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationStateUtils.kt
|
||
|
|
||
| fun addNotification(id: String, content: BannerContent) { | ||
| notifications[id] = content | ||
| } | ||
|
Check warning on line 75 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationStateUtils.kt
|
||
|
|
||
| fun getNotifications(): Map<String, BannerContent> = notifications | ||
|
Check warning on line 77 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationStateUtils.kt
|
||
|
|
||
| fun removeNotification(id: String) { | ||
| notifications.remove(id) | ||
| } | ||
|
Check warning on line 81 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationStateUtils.kt
|
||
|
|
||
| companion object { | ||
| fun getInstance(): BannerNotificationService = | ||
| service() | ||
|
Check warning on line 85 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationStateUtils.kt
|
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rli This feels hacky, but its one way to get around this issue, the other way I think we could maybe do this is to just set the expiry time to a very short window so that the file is always seen as expired by the time polling makes this call to the endpoint