Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
754a269
initial commit
samgst-amazon Nov 19, 2024
bca91eb
run on startup
samgst-amazon Nov 20, 2024
d894d86
detekt
samgst-amazon Nov 20, 2024
6beeadc
move vals
samgst-amazon Nov 20, 2024
c233764
remote resource implementation
samgst-amazon Nov 20, 2024
6f6dd6d
comments
samgst-amazon Nov 20, 2024
6bf7007
detekt
samgst-amazon Nov 20, 2024
9d32c86
Validate file before saving
samgst-amazon Nov 21, 2024
7952d9b
cache path
samgst-amazon Nov 21, 2024
fd2d1fb
observer implementation
samgst-amazon Nov 21, 2024
cff580f
deserialize notifs from file
samgst-amazon Nov 21, 2024
2d2ffaf
detekt
samgst-amazon Nov 21, 2024
a08194c
remove unused interface
samgst-amazon Nov 22, 2024
17b49fc
internal class
samgst-amazon Nov 22, 2024
c5718f7
Fix observer
samgst-amazon Nov 22, 2024
691d131
etag singleton state component
samgst-amazon Nov 22, 2024
4e588bf
add telemetry
samgst-amazon Nov 22, 2024
a4427ab
atomicBoolean
samgst-amazon Nov 22, 2024
2ce16c3
initialize once per IDE startup
samgst-amazon Nov 22, 2024
c54884d
code scan
samgst-amazon Nov 22, 2024
5f140d4
Omit (Unit)
samgst-amazon Nov 22, 2024
b8b6950
specify etag storage location
samgst-amazon Nov 22, 2024
098bb78
detekt
samgst-amazon Nov 22, 2024
ce834c8
fix detekt issues
samgst-amazon Nov 22, 2024
50ef8fd
basic tests
samgst-amazon Nov 23, 2024
6c76ec4
no star imports
samgst-amazon Nov 23, 2024
cc02c33
coroutine scope delay instead of thread.sleep
samgst-amazon Nov 25, 2024
f04da83
feedback fixes
samgst-amazon Nov 25, 2024
f81d96f
test fix
samgst-amazon Nov 25, 2024
9375226
Application Exists for tests
samgst-amazon Nov 25, 2024
7add727
endpoint object
samgst-amazon Nov 25, 2024
35a2b69
merge conflict
samgst-amazon Nov 25, 2024
aacc8e8
detekt
samgst-amazon Nov 25, 2024
d75d8e9
detekt fixes
samgst-amazon Nov 25, 2024
a14e6c1
boolean flag
samgst-amazon Nov 25, 2024
cbc2f4d
boolean flag
samgst-amazon Nov 25, 2024
580674e
update tests
samgst-amazon Nov 25, 2024
fcd46a6
move startup flag handling to processBase
samgst-amazon Nov 26, 2024
8fba3a3
fix delay
samgst-amazon Nov 26, 2024
84ed1ef
fix delay
samgst-amazon Nov 26, 2024
26caf31
Notification dismissal state tracking (#5129)
samgst-amazon Nov 27, 2024
8546d4a
endpoint as registryKey
samgst-amazon Nov 27, 2024
22420b1
detekt
samgst-amazon Nov 27, 2024
01bb9ef
Merge branch 'feature/ideNotifs' into samgst/notificationPolling
samgst-amazon Nov 27, 2024
7097fd9
fix startup issues
samgst-amazon Nov 27, 2024
49924d5
Expiry issues
samgst-amazon Nov 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// 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

Check warning on line 6 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused import directive

Unused import directive
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper

Check warning on line 7 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused import directive

Unused import directive
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)
fun dispose()
}

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 50 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Extension class should be final and non-public

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>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably have a separate persistent state class that keeps track of the etag and get its instance here

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suspend

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
getCachedPath()?.let { path ->
notifyObservers(path)
}
}
return false
}
val resolvedPath = resourceResolver.get()
.resolve(notificationsResource)
.toCompletableFuture()
.get()

Check warning

Code 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)
}
}
}
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? =

Check notice on line 128 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Class member can have 'private' visibility

Function 'getCachedPath' could be private
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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Extension class should be final and non-public

Extension class should not be public

override suspend fun execute(project: Project) {
val service = NotificationPollingServiceImpl.getInstance()
RunOnceUtil.runOnceForApp(this::class.qualifiedName.toString()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this? Will this startup activity anyway run only once?

Copy link
Contributor Author

@samgst-amazon samgst-amazon Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to Atomic Boolean, but the same sort of logic applies. Because StartupActivity is obsolete in favor of ProjectActivity, we need to ensure we are only creating one instance of the polling service per IDE session. Otherwise, when a user opens multiple projects, it would start multiple instances of the polling service

Tried registering the service as an ApplicationInitializedListener, but wasn't able to get that working.

service.startPolling()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,30 @@

package software.aws.toolkits.jetbrains.core.notifications

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.intellij.openapi.project.Project
import software.aws.toolkits.core.utils.inputStream
import java.io.InputStream

Check warning on line 11 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/ProcessNotificationsBase.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused import directive

Unused import directive
import java.nio.file.Path

object NotificationMapperUtil{
val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)

Check notice on line 15 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/ProcessNotificationsBase.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Function or property has platform type

Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable.
}

class ProcessNotificationsBase {
init {
// TODO: install a listener for the polling class
NotificationPollingServiceImpl.getInstance().addObserver { path ->
getNotificationsFromFile(path)
}
}

fun getNotificationsFromFile() {
// TODO: returns a notification list
}
fun getNotificationsFromFile(path: Path): NotificationsList =

Check notice on line 25 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/ProcessNotificationsBase.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Class member can have 'private' visibility

Function 'getNotificationsFromFile' could be private
path.inputStream().use { data ->
NotificationMapperUtil.mapper.readValue<NotificationsList>(data)
}


fun retrieveStartupAndEmergencyNotifications() {
// TODO: separates notifications into startup and emergency
Expand Down
4 changes: 4 additions & 0 deletions plugins/core/src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
<!-- each plugin needs its own instance of these -->
<applicationService serviceImplementation="migration.software.aws.toolkits.jetbrains.core.coroutines.PluginCoroutineScopeTracker"/>
<projectService serviceImplementation="migration.software.aws.toolkits.jetbrains.core.coroutines.PluginCoroutineScopeTracker"/>
<postStartupActivity implementation = "software.aws.toolkits.jetbrains.core.notifications.NotificationServiceInitializer"/>
<applicationService
serviceInterface="software.aws.toolkits.jetbrains.core.notifications.NotificationPollingService"
serviceImplementation="software.aws.toolkits.jetbrains.core.notifications.NotificationPollingServiceImpl"/>
</extensions>
<projectListeners>
<listener class="software.aws.toolkits.jetbrains.services.telemetry.OpenedFileTypesMetricsListener" topic="com.intellij.openapi.fileEditor.FileEditorManagerListener"/>
Expand Down
Loading