Skip to content
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

Introduce ViewRouter Component #337

Merged
merged 31 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
13c55ac
improved handling of lsp failure state
Dec 12, 2024
548a9da
add static mock to lsp connection test case
Dec 13, 2024
374e549
Integrate Event Broker in LspStatusManager
taldekar Jan 27, 2025
6a9f8d3
Add ViewRouter POC
taldekar Jan 28, 2025
d1746a4
Fix code formatting bug
taldekar Jan 28, 2025
091b6e9
Add method to retrieve observables
taldekar Jan 29, 2025
fa11959
Add listeners for combined state streams and view update request
taldekar Jan 29, 2025
f934e6c
Add listeners for combined state streams and view update request
taldekar Jan 29, 2025
a095805
Add listeners for combined state streams and view update request
taldekar Jan 29, 2025
4ad24a1
Add functionality for hot event streams that track latest event (#340)
taldekar Jan 29, 2025
d8b62b4
Remove active view update request listener
taldekar Jan 30, 2025
eca096f
Add ViewRouter tests
taldekar Jan 30, 2025
d1096a6
Remove public constructor
taldekar Jan 30, 2025
24f9c21
Remove public constructor and unused event in tests
taldekar Jan 30, 2025
eb53a8c
Add comments
taldekar Jan 30, 2025
fb1600e
Add documentation for EventBroker
taldekar Jan 30, 2025
da7b9f9
Remove ViewRouter initialization
taldekar Jan 30, 2025
6cea24d
Remove LspInitializingView from ID enum
taldekar Jan 30, 2025
17c0ebb
Add documentation for ViewRouter
taldekar Jan 30, 2025
f4da1d3
Refactor and enhance EventBroker tests
taldekar Jan 30, 2025
65c07f7
Refactor ViewRouter tests for clarity
taldekar Jan 30, 2025
9f01b0b
Remove PluginState class into separate file
taldekar Jan 30, 2025
ebdbcbf
Add documentation to subscription management logic
taldekar Jan 31, 2025
0276e79
Add support for notifying multiple late-subscribers over time of lat…
taldekar Jan 31, 2025
78d7439
Remove CODE_REFERENCE_VIEW
taldekar Jan 31, 2025
77fc653
Rename newActiveViewId to newActiveView
taldekar Jan 31, 2025
581b0eb
Rename ViewId class to AmazonQViewType
taldekar Jan 31, 2025
b395464
Revert "Integrate Event Broker in LspStatusManager"
taldekar Jan 31, 2025
c49a354
Revert "improved handling of lsp failure state"
taldekar Jan 31, 2025
4ed1bf1
Refactor EventBroker and enhance tests (#343)
taldekar Feb 4, 2025
3b95fcd
Merge branch 'feature/viewRefactor' into taldekar/ViewRouter
taldekar Feb 4, 2025
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
2 changes: 1 addition & 1 deletion plugin/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="3.31.0",
org.apache.commons.logging;bundle-version="1.2.0",
slf4j.api;bundle-version="2.0.13",
org.apache.commons.lang3;bundle-version="3.14.0"
Bundle-Classpath: target/classes/,
Bundle-Classpath: .,
target/dependency/annotations-2.28.26.jar,
target/dependency/apache-client-2.28.26.jar,
target/dependency/auth-2.28.26.jar,
Expand Down
12 changes: 12 additions & 0 deletions plugin/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
icon="icons/AmazonQ.png"
class="software.aws.toolkits.eclipse.amazonq.views.ChatAssetMissingView">
</view>
<view
id="software.aws.toolkits.eclipse.amazonq.views.LspStartUpFailedView"
name="Amazon Q"
icon="icons/AmazonQ.png"
class="software.aws.toolkits.eclipse.amazonq.views.LspStartUpFailedView">
</view>
<view
id="software.aws.toolkits.eclipse.amazonq.views.ToolkitLoginWebview"
name="Amazon Q"
Expand Down Expand Up @@ -142,6 +148,12 @@
relationship="stack"
visible="false">
</view>
<view
id="software.aws.toolkits.eclipse.amazonq.views.LspStartUpFailedView"
relative="software.aws.toolkits.eclipse.amazonq.views.ToolkitLoginWebview"
relationship="stack"
visible="false">
</view>
</perspectiveExtension>
</extension>
<extension
Expand Down
132 changes: 118 additions & 14 deletions plugin/src/software/aws/toolkits/eclipse/amazonq/broker/EventBroker.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,139 @@

package software.aws.toolkits.eclipse.amazonq.broker;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
import io.reactivex.rxjava3.subjects.Subject;
import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver;

/**
* A thread-safe event broker that manages event publishing and subscription
* using RxJava. This class provides a centralized mechanism for event handling
* across the application, with support for type-safe event publishing and
* subscription.
*/
public final class EventBroker {

private final Subject<Object> eventBus = PublishSubject.create().toSerialized();
private final Subject<Object> eventBus; // the main event bus
private final CompositeDisposable disposableSubscriptions;

/**
* Cache of type-specific observable streams. Each stream maintains its last
* emitted value and is "hot" due to eagerly connecting which allows for
* publishers to emit values event if no downstream publishers exist.
*/
private final Map<Class<?>, Observable<?>> cachedStatefulObservablesForType;

public EventBroker() {
eventBus = BehaviorSubject.create().toSerialized(); // serialize for thread safety
eventBus.subscribeOn(Schedulers.computation()); // publish on dedicated thread

cachedStatefulObservablesForType = new ConcurrentHashMap<>();
/*
* This hook runs before events are published and creates a
* ConnectableObservable before eagerly connecting to it to ensure that events
* get published to the stream regardless of whether the stream has subscribers
* allowing for events to be cached for late subscribers:
*/
eventBus.doOnNext(event -> getOrCreateObservable(event.getClass())).subscribe();

/**
* Initialize subscription management and set up automatic tracking of event bus
* subscriptions.
*/
disposableSubscriptions = new CompositeDisposable();

/**
* Configure the event bus to automatically track all new subscriptions.
* This ensures that all subscriptions created by the event bus are properly managed
* and can be disposed of when needed.
*
* The setup:
* 1. Hooks into the event bus subscription lifecycle using doOnSubscribe
* 2. Automatically adds each new subscription to the CompositeDisposable
* 3. Subscribes to start the subscription tracking
*/
eventBus.doOnSubscribe(subscription -> disposableSubscriptions.add(subscription)).subscribe();
}

/**
* Posts an event to the event bus. The event will be delivered to all
* subscribers of the specific event type or cached for late subscribers.
*
* @param <T> the type of the event
* @param event the event to publish (must not be null)
*/
public <T> void post(final T event) {
if (event == null) {
return;
}
eventBus.onNext(event);
}

/**
* Gets or creates an Observable for the specified event type. The
* ConnectedObservable maintains the last emitted value in the stream and
* autoConnecting to the stream ensures that events are published regardless of
* whether subscribers exist downstream. When events are emitted the latest
* value is also cached for replay when late subscribers join.
*
* @param <T> the type of events the Observable will emit
* @param eventType the Class object representing the event type
* @return an Observable that emits events of the specified type
*/
@SuppressWarnings("unchecked")
private <T> Observable<T> getOrCreateObservable(final Class<T> eventType) { // maintain stateful observables
return (Observable<T>) cachedStatefulObservablesForType.computeIfAbsent(eventType,
type -> eventBus.ofType(eventType).replay(1).autoConnect(-1)); // connect to stream immediately
}

/**
* Subscribes an observer to events of a specific type. The observer will
* receive events on a computation thread by default. The subscription is
* automatically tracked for disposal management.
*
* @param <T> the type of events to observe
* @param eventType the Class object representing the event type
* @param observer the observer that will handle emitted events
* @return a Disposable that can be used to unsubscribe from the events
*/
public <T> Disposable subscribe(final Class<T> eventType, final EventObserver<T> observer) {
Consumer<T> consumer = new Consumer<>() {
@Override
public void accept(final T event) {
observer.onEvent(event);
}
};

return eventBus.ofType(eventType)
.observeOn(Schedulers.computation())
.subscribe(consumer);
}
Disposable subscription = getOrCreateObservable(eventType)
.observeOn(Schedulers.computation()) // subscribe on dedicated thread
.subscribe(observer::onEvent);
disposableSubscriptions.add(subscription);
return subscription;
}

/**
* Returns an Observable for the specified event type. This Observable can be
* used to create custom subscription chains with additional operators.
*
* @param <T> the type of events the Observable will emit
* @param eventType the Class object representing the event type
* @return an Observable that emits events of the specified type
*/
public <T> Observable<T> ofObservable(final Class<T> eventType) {
return getOrCreateObservable(eventType);
}

/**
* Disposes of all subscriptions managed by this broker by clearing the disposable subscriptions collection.
* This method should be called when the broker is no longer needed to prevent memory leaks.
* After disposal, any existing subscriptions will be terminated and new events will not be delivered
* to their observers.
*
* Note: This only disposes of the subscriptions, not the underlying Observables.
* The EventBroker can be reused after disposal by creating new subscriptions.
*/
public void dispose() {
disposableSubscriptions.clear();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import software.aws.toolkits.eclipse.amazonq.chat.models.GenericCommandParams;
import software.aws.toolkits.eclipse.amazonq.chat.models.SendToPromptParams;
import software.aws.toolkits.eclipse.amazonq.chat.models.TriggerType;
import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspStatusManager;
import software.aws.toolkits.eclipse.amazonq.plugin.Activator;
import software.aws.toolkits.eclipse.amazonq.telemetry.ToolkitTelemetryProvider;
import software.aws.toolkits.eclipse.amazonq.telemetry.metadata.ExceptionMetadata;
Expand All @@ -27,7 +28,7 @@ public abstract class AbstractQChatEditorActionsHandler extends AbstractHandler
@Override
public final boolean isEnabled() {
try {
return Activator.getLoginService().getAuthState().isLoggedIn();
return Activator.getLoginService().getAuthState().isLoggedIn() && !LspStatusManager.getInstance().lspFailed();
} catch (Exception e) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import software.aws.toolkits.eclipse.amazonq.plugin.Activator;

import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspStatusManager;
import software.aws.toolkits.eclipse.amazonq.views.ViewVisibilityManager;

public class QOpenLoginViewHandler extends AbstractHandler {
@Override
public final Object execute(final ExecutionEvent event) {
if (Activator.getLoginService().getAuthState().isLoggedIn()) {
ViewVisibilityManager.showChatView("statusBar");
if (LspStatusManager.getInstance().lspFailed()) {
ViewVisibilityManager.showLspStartUpFailedView("statusBar");
} else {
ViewVisibilityManager.showLoginView("statusBar");
ViewVisibilityManager.showDefaultView("statusBar");
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,39 @@
import software.amazon.awssdk.utils.StringUtils;
import software.aws.toolkits.eclipse.amazonq.lsp.encryption.DefaultLspEncryptionManager;
import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspManager;
import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspStatusManager;
import software.aws.toolkits.eclipse.amazonq.lsp.manager.fetcher.RecordLspSetupArgs;
import software.aws.toolkits.eclipse.amazonq.plugin.Activator;
import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage;
import software.aws.toolkits.eclipse.amazonq.providers.LspManagerProvider;
import software.aws.toolkits.eclipse.amazonq.telemetry.LanguageServerTelemetryProvider;
import software.aws.toolkits.eclipse.amazonq.telemetry.metadata.ExceptionMetadata;
import software.aws.toolkits.eclipse.amazonq.util.ProxyUtil;
import software.aws.toolkits.telemetry.TelemetryDefinitions.Result;
import software.aws.toolkits.eclipse.amazonq.plugin.Activator;
import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage;

public class QLspConnectionProvider extends AbstractLspConnectionProvider {

public QLspConnectionProvider() throws IOException {
super();
LanguageServerTelemetryProvider.setAllStartPoint(Instant.now());
LspManager lspManager = LspManagerProvider.getInstance();
var lspInstallResult = lspManager.getLspInstallation();
try {
LanguageServerTelemetryProvider.setAllStartPoint(Instant.now());
LspManager lspManager = LspManagerProvider.getInstance();
var lspInstallResult = lspManager.getLspInstallation();

setWorkingDirectory(lspInstallResult.getServerDirectory());

setWorkingDirectory(lspInstallResult.getServerDirectory());
var serverCommand = Paths.get(lspInstallResult.getServerDirectory(), lspInstallResult.getServerCommand());
List<String> commands = new ArrayList<>();
commands.add(serverCommand.toString());
commands.add(lspInstallResult.getServerCommandArgs());
commands.add("--stdio");
commands.add("--set-credentials-encryption-key");
setCommands(commands);
} catch (Exception e) {
LspStatusManager.getInstance().setToFailed();
throw(e);
}

var serverCommand = Paths.get(lspInstallResult.getServerDirectory(), lspInstallResult.getServerCommand());
List<String> commands = new ArrayList<>();
commands.add(serverCommand.toString());
commands.add(lspInstallResult.getServerCommandArgs());
commands.add("--stdio");
commands.add("--set-credentials-encryption-key");
setCommands(commands);
}

@Override
Expand Down Expand Up @@ -70,10 +77,12 @@ public final void start() throws IOException {

lspEncryption.initializeEncryptedCommunication(serverStdIn);
} catch (Exception e) {
LspStatusManager.getInstance().setToFailed();
emitInitFailure(ExceptionMetadata.scrubException(e));
Activator.getLogger().error("Error occured while initializing communication with Amazon Q Lsp Server", e);
}
} catch (Exception e) {
LspStatusManager.getInstance().setToFailed();
emitInitFailure(ExceptionMetadata.scrubException(e));
throw e;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.eclipse.amazonq.lsp.manager;

public enum LspState {
ACTIVE,
FAILED,
PENDING
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.eclipse.amazonq.lsp.manager;

import software.aws.toolkits.eclipse.amazonq.plugin.Activator;
import software.aws.toolkits.eclipse.amazonq.views.ViewVisibilityManager;

public final class LspStatusManager {

private static final LspStatusManager INSTANCE;
private LspState lspState;

static {
INSTANCE = new LspStatusManager();
}

private LspStatusManager() {
lspState = LspState.PENDING;
Activator.getEventBroker().post(lspState);
}

public static LspStatusManager getInstance() {
return INSTANCE;
}


public boolean lspFailed() {
return (lspState == LspState.FAILED);
}

public void setToActive() {
lspState = LspState.ACTIVE;
Activator.getEventBroker().post(lspState);
ViewVisibilityManager.showDefaultView("restart");
}

public void setToFailed() {
if (lspState != LspState.FAILED) {
ViewVisibilityManager.showLspStartUpFailedView("update");
lspState = LspState.FAILED;
}

Activator.getEventBroker().post(lspState);
}
public LspState getLspState() {
return lspState;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.eclipse.lsp4j.services.LanguageServer;

import software.aws.toolkits.eclipse.amazonq.lsp.AmazonQLspServer;
import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspStatusManager;
import software.aws.toolkits.eclipse.amazonq.lsp.manager.fetcher.RecordLspSetupArgs;
import software.aws.toolkits.eclipse.amazonq.telemetry.LanguageServerTelemetryProvider;
import software.aws.toolkits.telemetry.TelemetryDefinitions.Result;
Expand Down Expand Up @@ -52,6 +53,7 @@ public void setAmazonQServer(final LanguageServer server) {
future.complete(server);
}
emitInitializeMetric();
LspStatusManager.getInstance().setToActive();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import software.aws.toolkits.eclipse.amazonq.configuration.PluginStoreKeys;
import software.aws.toolkits.eclipse.amazonq.lsp.AwsServerCapabiltiesProvider;
import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState;
import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspStatusManager;
import software.aws.toolkits.eclipse.amazonq.lsp.model.ChatOptions;
import software.aws.toolkits.eclipse.amazonq.lsp.model.QuickActions;
import software.aws.toolkits.eclipse.amazonq.lsp.model.QuickActionsCommandGroup;
Expand Down Expand Up @@ -141,7 +142,7 @@ public final void onEvent(final AuthState authState) {
// chat view
if (browser != null && !browser.isDisposed() && !chatStateManager.hasPreservedState()) {
Optional<String> content = getContent();
if (!content.isPresent()) {
if (!content.isPresent() && !LspStatusManager.getInstance().lspFailed()) {
canDisposeState = true;
ViewVisibilityManager.showChatAssetMissingView("update");
} else {
Expand Down
Loading