Live: Remove queryOverLive and live-service-web-worker experimental feature flags (#103518)

This commit is contained in:
Ryan McKinley
2025-04-07 17:47:35 +03:00
committed by GitHub
parent e2a6f9a849
commit 63af403897
17 changed files with 8 additions and 273 deletions
-3
View File
@@ -2653,9 +2653,6 @@ exports[`better eslint`] = {
"public/app/features/live/centrifuge/channel.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/live/centrifuge/serviceWorkerProxy.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/live/dashboard/DashboardChangedModal.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
@@ -124,8 +124,6 @@ Experimental features might be changed or removed without prior notice.
| Feature toggle name | Description |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread |
| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries |
| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) |
| `storage` | Configurable storage for dashboards, datasources, and resources |
| `canvasPanelNesting` | Allow elements nesting |
@@ -24,14 +24,6 @@ export interface FeatureToggles {
*/
disableEnvelopeEncryption?: boolean;
/**
* This will use a webworker thread to processes events rather than the main thread
*/
['live-service-web-worker']?: boolean;
/**
* Use Grafana Live WebSocket to execute backend queries
*/
queryOverLive?: boolean;
/**
* Search for dashboards using panel title
*/
panelTitleSearch?: boolean;
@@ -72,15 +72,6 @@ export interface GrafanaLiveSrv {
*/
getDataStream(options: LiveDataStreamOptions): Observable<DataQueryResponse>;
/**
* Execute a query over the live websocket and potentiall subscribe to a live channel.
*
* Since the initial request and subscription are on the same socket, this will support HA setups
*
* @alpha -- this function requires the feature toggle `queryOverLive` to be set
*/
getQueryData(options: LiveQueryDataOptions): Observable<DataQueryResponse>;
/**
* For channels that support presence, this will request the current state from the server.
*
@@ -196,13 +196,6 @@ class DataSourceWithBackend<
to: range?.to.valueOf().toString(),
};
if (config.featureToggles.queryOverLive) {
return getGrafanaLiveSrv().getQueryData({
request,
body,
});
}
const headers: Record<string, string> = request.headers ?? {};
headers[PluginRequestHeaders.PluginID] = Array.from(pluginIDs).join(', ');
headers[PluginRequestHeaders.DatasourceUID] = Array.from(dsUIDs).join(', ');
-14
View File
@@ -27,20 +27,6 @@ var (
AllowSelfServe: false,
Expression: "false",
},
{
Name: "live-service-web-worker",
Description: "This will use a webworker thread to processes events rather than the main thread",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
},
{
Name: "queryOverLive",
Description: "Use Grafana Live WebSocket to execute backend queries",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
},
{
Name: "panelTitleSearch",
Description: "Search for dashboards using panel title",
-2
View File
@@ -1,7 +1,5 @@
Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
disableEnvelopeEncryption,GA,@grafana/grafana-as-code,false,false,false
live-service-web-worker,experimental,@grafana/dashboards-squad,false,false,true
queryOverLive,experimental,@grafana/dashboards-squad,false,false,true
panelTitleSearch,preview,@grafana/search-and-storage,false,false,false
publicDashboardsEmailSharing,preview,@grafana/sharing-squad,false,false,false
publicDashboardsScene,GA,@grafana/sharing-squad,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
2 disableEnvelopeEncryption GA @grafana/grafana-as-code false false false
live-service-web-worker experimental @grafana/dashboards-squad false false true
queryOverLive experimental @grafana/dashboards-squad false false true
3 panelTitleSearch preview @grafana/search-and-storage false false false
4 publicDashboardsEmailSharing preview @grafana/sharing-squad false false false
5 publicDashboardsScene GA @grafana/sharing-squad false false true
-8
View File
@@ -11,14 +11,6 @@ const (
// Disable envelope encryption (emergency only)
FlagDisableEnvelopeEncryption = "disableEnvelopeEncryption"
// FlagLiveServiceWebWorker
// This will use a webworker thread to processes events rather than the main thread
FlagLiveServiceWebWorker = "live-service-web-worker"
// FlagQueryOverLive
// Use Grafana Live WebSocket to execute backend queries
FlagQueryOverLive = "queryOverLive"
// FlagPanelTitleSearch
// Search for dashboards using panel title
FlagPanelTitleSearch = "panelTitleSearch"
+4 -2
View File
@@ -1726,7 +1726,8 @@
"metadata": {
"name": "live-service-web-worker",
"resourceVersion": "1743693517832",
"creationTimestamp": "2022-01-26T17:44:20Z"
"creationTimestamp": "2022-01-26T17:44:20Z",
"deletionTimestamp": "2025-04-07T09:56:17Z"
},
"spec": {
"description": "This will use a webworker thread to processes events rather than the main thread",
@@ -2588,7 +2589,8 @@
"metadata": {
"name": "queryOverLive",
"resourceVersion": "1743693517832",
"creationTimestamp": "2022-01-26T17:44:20Z"
"creationTimestamp": "2022-01-26T17:44:20Z",
"deletionTimestamp": "2025-04-07T09:56:17Z"
},
"spec": {
"description": "Use Grafana Live WebSocket to execute backend queries",
@@ -1,3 +0,0 @@
import { CorsWorker as Worker } from 'app/core/utils/CorsWorker';
export const createWorker = () => new Worker(new URL('./service.worker.ts', import.meta.url));
@@ -1,33 +0,0 @@
import * as comlink from 'comlink';
import { from, Observable, switchMap } from 'rxjs';
export const remoteObservableAsObservable = <T>(remoteObs: comlink.RemoteObject<Observable<T>>): Observable<T> =>
new Observable((subscriber) => {
// Passing the callbacks as 3 separate arguments is deprecated, but it's the only option for now
//
// RxJS recreates the functions via `Function.bind` https://github.com/ReactiveX/rxjs/blob/62aca850a37f598b5db6085661e0594b81ec4281/src/internal/Subscriber.ts#L169
// and thus erases the ProxyMarker created via comlink.proxy(fN) when the callbacks
// are grouped together in a Observer object (ie. { next: (v) => ..., error: (err) => ..., complete: () => ... })
//
// solution: TBD (autoproxy all functions?)
const remoteSubPromise = remoteObs.subscribe(
comlink.proxy((nextValueInRemoteObs: T) => {
subscriber.next(nextValueInRemoteObs);
}),
comlink.proxy((err: unknown) => {
subscriber.error(err);
}),
comlink.proxy(() => {
subscriber.complete();
})
);
return {
unsubscribe: () => {
remoteSubPromise.then((remoteSub) => remoteSub.unsubscribe());
},
};
});
export const promiseWithRemoteObservableAsObservable = <T>(
promiseWithProxyObservable: Promise<comlink.RemoteObject<Observable<T>>>
): Observable<T> => from(promiseWithProxyObservable).pipe(switchMap((val) => remoteObservableAsObservable(val)));
@@ -1,64 +0,0 @@
import './transferHandlers';
import * as comlink from 'comlink';
import { LiveChannelAddress } from '@grafana/data';
import { LiveDataStreamOptions, LivePublishOptions, LiveQueryDataOptions } from '@grafana/runtime';
import { remoteObservableAsObservable } from './remoteObservable';
import { CentrifugeService, CentrifugeSrvDeps } from './service';
let centrifuge: CentrifugeService;
const initialize = (
deps: CentrifugeSrvDeps,
remoteDataStreamSubscriberReadiness: comlink.RemoteObject<
CentrifugeSrvDeps['dataStreamSubscriberReadiness'] & comlink.ProxyMarked
>
) => {
centrifuge = new CentrifugeService({
...deps,
dataStreamSubscriberReadiness: remoteObservableAsObservable(remoteDataStreamSubscriberReadiness),
});
};
const getConnectionState = () => {
return comlink.proxy(centrifuge.getConnectionState());
};
const getDataStream = (options: LiveDataStreamOptions) => {
return comlink.proxy(centrifuge.getDataStream(options));
};
const getQueryData = async (options: LiveQueryDataOptions) => {
return await centrifuge.getQueryData(options);
};
const getStream = (address: LiveChannelAddress) => {
return comlink.proxy(centrifuge.getStream(address));
};
const getPresence = async (address: LiveChannelAddress) => {
return await centrifuge.getPresence(address);
};
const publish = (address: LiveChannelAddress, data: unknown, options?: LivePublishOptions) =>
centrifuge.publish(address, data, options);
const workObj = {
initialize,
getConnectionState,
getDataStream,
getStream,
getQueryData,
getPresence,
publish,
};
export type RemoteCentrifugeService = typeof workObj;
comlink.expose(workObj);
export default class {
constructor() {}
}
@@ -1,54 +0,0 @@
import './transferHandlers';
import * as comlink from 'comlink';
import { asyncScheduler, Observable, observeOn } from 'rxjs';
import { LiveChannelAddress, LiveChannelEvent } from '@grafana/data';
import { createWorker } from './createCentrifugeServiceWorker';
import { promiseWithRemoteObservableAsObservable } from './remoteObservable';
import { CentrifugeSrv, CentrifugeSrvDeps } from './service';
import { RemoteCentrifugeService } from './service.worker';
export class CentrifugeServiceWorkerProxy implements CentrifugeSrv {
private centrifugeWorker;
constructor(deps: CentrifugeSrvDeps) {
this.centrifugeWorker = comlink.wrap<RemoteCentrifugeService>(createWorker());
this.centrifugeWorker.initialize(deps, comlink.proxy(deps.dataStreamSubscriberReadiness));
}
getConnectionState: CentrifugeSrv['getConnectionState'] = () => {
return promiseWithRemoteObservableAsObservable(this.centrifugeWorker.getConnectionState());
};
getDataStream: CentrifugeSrv['getDataStream'] = (options) => {
return promiseWithRemoteObservableAsObservable(this.centrifugeWorker.getDataStream(options)).pipe(
// async scheduler splits the synchronous task of deserializing data from web worker and
// consuming the message (ie. updating react component) into two to avoid blocking the event loop
observeOn(asyncScheduler)
);
};
/**
* Query over websocket
*/
getQueryData: CentrifugeSrv['getQueryData'] = async (options) => {
const optionsAsPlainSerializableObject = JSON.parse(JSON.stringify(options));
return this.centrifugeWorker.getQueryData(optionsAsPlainSerializableObject);
};
getPresence: CentrifugeSrv['getPresence'] = (address) => {
return this.centrifugeWorker.getPresence(address);
};
getStream: CentrifugeSrv['getStream'] = <T>(address: LiveChannelAddress) => {
return promiseWithRemoteObservableAsObservable(
this.centrifugeWorker.getStream(address) as Promise<comlink.Remote<Observable<LiveChannelEvent<T>>>>
);
};
publish: CentrifugeSrv['publish'] = (address, data, options) => {
return this.centrifugeWorker.publish(address, data, options);
};
}
@@ -1,27 +0,0 @@
import * as comlink from 'comlink';
import { Subscriber } from 'rxjs';
// Observers, ie. functions passed to `observable.subscribe(...)`, are converted to a subclass of `Subscriber` before they are sent to the source Observable.
// The conversion happens internally in the RxJS library - this transfer handler is catches them and wraps them with a proxy
const subscriberTransferHandler = {
canHandle(value: unknown): value is Subscriber<unknown> {
return Boolean(value && value instanceof Subscriber);
},
serialize(value: Function): [MessagePort, Transferable[]] {
const obj = comlink.proxy(value);
const { port1, port2 } = new MessageChannel();
comlink.expose(obj, port1);
return [port2, [port2]];
},
deserialize(value: MessagePort): comlink.Remote<MessagePort> {
value.start();
return comlink.wrap<MessagePort>(value);
},
};
comlink.transferHandlers.set('SubscriberHandler', subscriberTransferHandler);
+1 -4
View File
@@ -5,7 +5,6 @@ import { contextSrv } from '../../core/services/context_srv';
import { loadUrlToken } from '../../core/utils/urlToken';
import { CentrifugeService } from './centrifuge/service';
import { CentrifugeServiceWorkerProxy } from './centrifuge/serviceWorkerProxy';
import { GrafanaLiveService } from './live';
export function initGrafanaLive() {
@@ -18,9 +17,7 @@ export function initGrafanaLive() {
grafanaAuthToken: loadUrlToken(),
};
const centrifugeSrv = config.featureToggles['live-service-web-worker']
? new CentrifugeServiceWorkerProxy(centrifugeServiceDeps)
: new CentrifugeService(centrifugeServiceDeps);
const centrifugeSrv = new CentrifugeService(centrifugeServiceDeps);
setGrafanaLiveSrv(
new GrafanaLiveService({
+3 -24
View File
@@ -1,8 +1,7 @@
import { from, map, of, switchMap } from 'rxjs';
import { map } from 'rxjs';
import { DataFrame, toLiveChannelId, StreamingDataFrame } from '@grafana/data';
import { BackendSrv, GrafanaLiveSrv, toDataQueryResponse } from '@grafana/runtime';
import { standardStreamOptionsProvider, toStreamingDataResponse } from '@grafana/runtime/internal';
import { toLiveChannelId, StreamingDataFrame } from '@grafana/data';
import { BackendSrv, GrafanaLiveSrv } from '@grafana/runtime';
import { CentrifugeSrv, StreamingDataQueryResponse } from './centrifuge/service';
import { isStreamingResponseData, StreamingResponseDataType } from './data/utils';
@@ -65,26 +64,6 @@ export class GrafanaLiveService implements GrafanaLiveSrv {
return this.deps.centrifugeSrv.getStream(address);
};
/**
* Execute a query over the live websocket and potentially subscribe to a live channel.
*
* Since the initial request and subscription are on the same socket, this will support HA setups
*/
getQueryData: GrafanaLiveSrv['getQueryData'] = (options) => {
return from(this.deps.centrifugeSrv.getQueryData(options)).pipe(
switchMap((rawResponse) => {
const parsedResponse = toDataQueryResponse(rawResponse, options.request.targets);
const isSubscribable =
parsedResponse.data?.length && parsedResponse.data.find((f: DataFrame) => f.meta?.channel);
return isSubscribable
? toStreamingDataResponse(parsedResponse, options.request, standardStreamOptionsProvider)
: of(parsedResponse);
})
);
};
/**
* Publish into a channel
*
-9
View File
@@ -25,12 +25,3 @@ class LayoutMockWorker {
jest.mock('../../app/plugins/panel/nodeGraph/createLayoutWorker', () => ({
createWorker: () => new LayoutMockWorker(),
}));
class BasicMockWorker {
postMessage() {}
}
const mockCreateWorker = {
createWorker: () => new BasicMockWorker(),
};
jest.mock('../../app/features/live/centrifuge/createCentrifugeServiceWorker', () => mockCreateWorker);