-
Notifications
You must be signed in to change notification settings - Fork 228
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* initial commit * run on startup * detekt * move vals * remote resource implementation * comments * detekt * Validate file before saving * cache path * observer implementation * deserialize notifs from file * detekt * remove unused interface * internal class * Fix observer * etag singleton state component * add telemetry * atomicBoolean * initialize once per IDE startup * code scan * Omit (Unit) * specify etag storage location * detekt * fix detekt issues * basic tests * no star imports * coroutine scope delay instead of thread.sleep * feedback fixes * test fix * Application Exists for tests * endpoint object * detekt * detekt fixes * boolean flag * boolean flag * update tests * move startup flag handling to processBase * fix delay * fix delay * Notification dismissal state tracking (#5129) * split notifications into separated lists. * add persistent notification dismissal state logic * boolean changes * group persistant states * comments * Service initialized automatically * isStartup global * Deserialized notification schedule type * tests * persistent state syntax * convert to light services * Remove state from companion object * detekt * endpoint as registryKey * detekt * fix startup issues * Expiry issues
- Loading branch information
1 parent
5f43277
commit 33bfa44
Showing
14 changed files
with
619 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
149 changes: 149 additions & 0 deletions
149
...nity/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
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 | ||
} | ||
} | ||
|
||
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() }, | ||
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() | ||
} | ||
return false | ||
} | ||
resourceResolver.get() | ||
.resolve(notificationsResource) | ||
.toCompletableFuture() | ||
.get() | ||
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++ | ||
if (retryCount < MAX_RETRIES) { | ||
val backoffDelay = RETRY_DELAY_MS * (1L shl (retryCount - 1)) | ||
delay(backoffDelay) | ||
} | ||
} | ||
} | ||
emitFailureMetric(lastException) | ||
return false | ||
} | ||
|
||
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" } | ||
throw e | ||
} | ||
|
||
private fun emitFailureMetric(e: Exception?) { | ||
ToolkitTelemetry.showNotification( | ||
project = null, | ||
component = Component.Filesystem, | ||
id = "", | ||
reason = "Failed to poll for notifications", | ||
success = false, | ||
reasonDesc = "${e?.javaClass?.simpleName ?: "Unknown"}: ${e?.message ?: "No message"}", | ||
) | ||
} | ||
|
||
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) | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
.../src/software/aws/toolkits/jetbrains/core/notifications/NotificationServiceInitializer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} | ||
} |
87 changes: 87 additions & 0 deletions
87
...ommunity/src/software/aws/toolkits/jetbrains/core/notifications/NotificationStateUtils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
override fun loadState(state: NotificationDismissalConfiguration) { | ||
this.state.dismissedNotificationIds.clear() | ||
this.state.dismissedNotificationIds.addAll(state.dismissedNotificationIds) | ||
} | ||
|
||
fun isDismissed(notificationId: String): Boolean = | ||
state.dismissedNotificationIds.contains(notificationId) | ||
|
||
fun dismissNotification(notificationId: String) { | ||
state.dismissedNotificationIds.add(notificationId) | ||
} | ||
|
||
companion object { | ||
fun getInstance(): NotificationDismissalState = | ||
service() | ||
} | ||
} | ||
|
||
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 | ||
|
||
override fun loadState(state: NotificationEtagConfiguration) { | ||
this.state.etag = state.etag | ||
} | ||
|
||
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>() | ||
|
||
fun addNotification(id: String, content: BannerContent) { | ||
notifications[id] = content | ||
} | ||
|
||
fun getNotifications(): Map<String, BannerContent> = notifications | ||
|
||
fun removeNotification(id: String) { | ||
notifications.remove(id) | ||
} | ||
|
||
companion object { | ||
fun getInstance(): BannerNotificationService = | ||
service() | ||
} | ||
} |
Oops, something went wrong.