-
Notifications
You must be signed in to change notification settings - Fork 274
Polling for new notifications #5110
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 24 commits
c3cc2bf
09729c7
f0dbe5b
e909a0e
5a84e39
56c1888
a981227
bd354fc
fea410a
746e5ab
b038d48
1d6e7d2
c4f3cac
867bcda
b7f336e
1883530
dbfcefc
823ea7f
218c8ed
285650f
d2b7a6c
c82ee66
4c397f4
033a3a9
b01283e
5506ca8
dd72d29
d6e7aa7
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,148 @@ | ||
| // 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.databind.DeserializationFeature | ||
| import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper | ||
Check warningCode scanning / QDJVMC Unused import directive Warning
Unused import directive
|
||
| import com.fasterxml.jackson.module.kotlin.readValue | ||
|
||
| 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.debug | ||
| 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 dispose() | ||
| } | ||
|
|
||
| object NotificationFileValidator : RemoteResolveParser { | ||
| private val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) | ||
| override fun canBeParsed(data: InputStream): Boolean { | ||
| return try { | ||
| mapper.readValue<NotificationsList>(data) | ||
| true | ||
| } catch (e: Exception) { | ||
| false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Service(Service.Level.APP) | ||
| class NotificationPollingServiceImpl : | ||
|
Check warning on line 50 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
|
||
Check warningCode scanning / QDJVMC Extension class should be final and non-public Warning
Service implementation should not be public. If a service is supposed to be used outside its module, extract an interface from it and specify it as serviceInterface in plugin.xml.
|
||
| NotificationPollingService, | ||
| PersistentStateComponent<NotificationPollingServiceImpl.State>, | ||
| Disposable { | ||
|
|
||
| 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 | ||
| } | ||
|
|
||
| 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() | ||
| } | ||
|
|
||
| override fun startPolling() { | ||
| pollForNotifications() | ||
| // todo notify observers | ||
| 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we add tests for this? |
||
| var retryCount = 0 | ||
| var lastException: Exception? = null | ||
|
|
||
| while (retryCount < MAX_RETRIES) { | ||
| try { | ||
| val newETag = getNotificationETag() | ||
| if (newETag == state.currentETag) { | ||
| LOG.debug { "No updates available for notifications" } | ||
| return false | ||
| } | ||
| val resolvedPath = resourceResolver.get() | ||
| .resolve(notificationsResource) | ||
| .toCompletableFuture() | ||
| .get() | ||
| 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) | ||
| } | ||
| } | ||
| } | ||
| emitFailureMetric(lastException) | ||
| return false | ||
| } | ||
|
|
||
| private fun getNotificationETag(): String = | ||
| HttpRequests.request(NOTIFICATION_ENDPOINT) | ||
| .userAgent("AWS Toolkit for JetBrains") | ||
| .connect { request -> | ||
| request.connection.headerFields["ETag"]?.firstOrNull() ?: "" | ||
| } | ||
|
|
||
| // Helper method to get Path from stored String | ||
| fun getCachedPath(): Path? = | ||
|
||
| state.cachedFilePath?.let { Paths.get(it) } | ||
|
|
||
| /** | ||
| * Emits telemetry metric for polling failures | ||
| */ | ||
| private fun emitFailureMetric(exception: Exception?) { | ||
| // todo: add metric | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this already exists in common, we can add this metric |
||
| } | ||
|
|
||
| companion object { | ||
| private val LOG = getLogger<NotificationPollingServiceImpl>() | ||
| fun getInstance(): NotificationPollingService = | ||
| ApplicationManager.getApplication().getService(NotificationPollingService::class.java) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // 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.application.ApplicationManager | ||
| import com.intellij.openapi.project.Project | ||
| import com.intellij.openapi.startup.ProjectActivity | ||
|
|
||
| class NotificationServiceInitializer : ProjectActivity { | ||
|
Check warning on line 11 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationServiceInitializer.kt
|
||
Check warningCode scanning / QDJVMC Extension class should be final and non-public Warning
Extension class should not be public
|
||
|
|
||
| override suspend fun execute(project: Project) { | ||
| val service = ApplicationManager.getApplication().getService(NotificationPollingService::class.java) | ||
|
||
| RunOnceUtil.runOnceForApp(this::class.qualifiedName.toString()) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this? Wont the startup activity anyway run just once?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added this to explicitly only run once per IDE instance and not per-project. It's what I understood to work, don't know about a better way yet |
||
| service.startPolling() | ||
| } | ||
| } | ||
| } | ||
Check warning
Code scanning / QDJVMC
Unused import directive Warning