-
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 12 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,161 @@ | ||
| // 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.ide.util.RunOnceUtil | ||
| import com.intellij.openapi.Disposable | ||
| import com.intellij.openapi.application.ApplicationManager | ||
| import com.intellij.openapi.components.PersistentStateComponent | ||
| import com.intellij.openapi.components.Service | ||
| import com.intellij.util.Alarm | ||
| import com.intellij.util.AlarmFactory | ||
| import com.intellij.util.io.HttpRequests | ||
| 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.jetbrains.core.DefaultRemoteResourceResolverProvider | ||
| import software.aws.toolkits.jetbrains.core.RemoteResourceResolverProvider | ||
| import java.io.InputStream | ||
| import java.nio.file.Path | ||
| import java.nio.file.Paths | ||
| import java.time.Duration | ||
|
|
||
| private const val NOTIFICATION_ENDPOINT = "https://idetoolkits-hostedfiles.amazonaws.com/Notifications/JetBrains/1.json" // TODO: Replace with actual endpoint | ||
| private const val MAX_RETRIES = 3 | ||
| private const val RETRY_DELAY_MS = 1000L | ||
|
|
||
| interface NotificationPollingService { | ||
| fun startPolling() | ||
| fun addObserver(observer: (Path) -> Unit) | ||
samgst-amazon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| fun dispose() | ||
samgst-amazon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| object NotificationFileValidator : RemoteResolveParser { | ||
| override fun canBeParsed(data: InputStream): Boolean { | ||
| return try { | ||
| NotificationMapperUtil.mapper.readValue<NotificationsList>(data) | ||
| true | ||
| } catch (e: Exception) { | ||
| false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Service(Service.Level.APP) | ||
| class NotificationPollingServiceImpl : | ||
|
Check warning on line 48 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
|
||
|
||
| NotificationPollingService, | ||
| PersistentStateComponent<NotificationPollingServiceImpl.State>, | ||
|
||
| Disposable { | ||
|
|
||
| private val observers = mutableListOf<(Path) -> Unit>() | ||
| private var state = State() | ||
| 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(NOTIFICATION_ENDPOINT) | ||
| override val remoteResolveParser: RemoteResolveParser = NotificationFileValidator | ||
| } | ||
|
|
||
| override fun startPolling() { | ||
| val newNotifications = pollForNotifications() | ||
| if (newNotifications) { | ||
| getCachedPath()?.let { path -> | ||
| notifyObservers(path) | ||
| } | ||
| } | ||
| alarm.addRequest( | ||
| { startPolling() }, | ||
| pollingIntervalMs | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Main polling function that checks for updates and downloads if necessary | ||
| * Returns the parsed notifications if successful, null otherwise | ||
| */ | ||
| private fun pollForNotifications(): Boolean { | ||
|
||
| var retryCount = 0 | ||
| var lastException: Exception? = null | ||
|
|
||
| while (retryCount < MAX_RETRIES) { | ||
| try { | ||
| val newETag = getNotificationETag() | ||
| if (newETag == state.currentETag) { | ||
| RunOnceUtil.runOnceForApp(this::class.qualifiedName.toString()) { | ||
| // try startup notifications regardless of file change | ||
samgst-amazon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| getCachedPath()?.let { path -> | ||
| notifyObservers(path) | ||
| } | ||
| } | ||
| return false | ||
| } | ||
| val resolvedPath = resourceResolver.get() | ||
| .resolve(notificationsResource) | ||
| .toCompletableFuture() | ||
| .get() | ||
Check warningCode scanning / QDJVMC Possibly blocking call in non-blocking context Warning
Possibly blocking call in non-blocking context could lead to thread starvation
|
||
| state.currentETag = newETag | ||
| state.cachedFilePath = resolvedPath.toString() | ||
| return true | ||
| } catch (e: Exception) { | ||
| lastException = e | ||
| LOG.error(e) { "Failed to poll for notifications (attempt ${retryCount + 1}/$MAX_RETRIES)" } | ||
| retryCount++ | ||
| if (retryCount < MAX_RETRIES) { | ||
| val backoffDelay = RETRY_DELAY_MS * (1L shl (retryCount - 1)) | ||
| Thread.sleep(backoffDelay) | ||
samgst-amazon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| } | ||
| emitFailureMetric(lastException) | ||
| return false | ||
| } | ||
|
|
||
| private fun getNotificationETag(): String = | ||
| HttpRequests.request(NOTIFICATION_ENDPOINT) | ||
samgst-amazon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| .userAgent("AWS Toolkit for JetBrains") | ||
| .connect { request -> | ||
| request.connection.headerFields["ETag"]?.firstOrNull() ?: "" | ||
| } | ||
|
|
||
| // Helper method to get Path from stored String | ||
| fun getCachedPath(): Path? = | ||
|
Check notice on line 126 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
|
||
|
||
| state.cachedFilePath?.let { Paths.get(it) } | ||
|
|
||
| override fun addObserver(observer: (Path) -> Unit) { | ||
| observers.add(observer) | ||
| } | ||
|
|
||
| private fun notifyObservers(path: Path) { | ||
| observers.forEach { it(path) } | ||
| } | ||
|
|
||
| private fun emitFailureMetric(exception: Exception?) { | ||
| // todo: add metric | ||
| } | ||
|
|
||
| companion object { | ||
| private val LOG = getLogger<NotificationPollingServiceImpl>() | ||
| fun getInstance(): NotificationPollingService = | ||
| ApplicationManager.getApplication().getService(NotificationPollingService::class.java) | ||
| } | ||
|
|
||
| data class State( | ||
| var currentETag: String? = null, | ||
| var cachedFilePath: String? = null, | ||
| ) | ||
|
|
||
| override fun getState(): State = state | ||
|
|
||
| override fun loadState(state: State) { | ||
| this.state = state | ||
| } | ||
|
|
||
| override fun dispose() { | ||
| alarm.dispose() | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| // 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.ide.util.RunOnceUtil | ||
| import com.intellij.openapi.project.Project | ||
| import com.intellij.openapi.startup.ProjectActivity | ||
|
|
||
| class NotificationServiceInitializer : ProjectActivity { | ||
|
Check warning on line 10 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationServiceInitializer.kt
|
||
|
||
|
|
||
| override suspend fun execute(project: Project) { | ||
| val service = NotificationPollingServiceImpl.getInstance() | ||
| RunOnceUtil.runOnceForApp(this::class.qualifiedName.toString()) { | ||
|
||
| service.startPolling() | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.