UNPKG

virool-pivot

Version:

A web-based exploratory visualization UI for Druid.io

270 lines (224 loc) 9.18 kB
import * as Q from 'q'; import { Duration, Timezone } from 'chronoshift'; import { $, AttributeInfo, RefExpression, DruidExternal, helper } from 'plywood'; import { makeUrlSafeName } from '../../../common/utils/general/general'; import { DataSource, RefreshRule } from '../../../common/models/index'; export type SourceListScan = "disable" | "auto"; export interface DataSourceLoader { (dataSource: DataSource): Q.Promise<DataSource>; } export interface DataSourceManagerOptions { dataSources?: DataSource[]; dataSourceStubFactory?: (source: string) => DataSource; dataSourceLoader?: DataSourceLoader; druidRequester?: Requester.PlywoodRequester<any>; pageMustLoadTimeout?: number; sourceListScan?: SourceListScan; sourceListRefreshOnLoad?: boolean; sourceListRefreshInterval?: number; sourceReintrospectOnLoad?: boolean; sourceReintrospectInterval?: number; log?: Function; } export interface DataSourceManager { getDataSources: () => Q.Promise<DataSource[]>; getQueryableDataSources: () => Q.Promise<DataSource[]>; getQueryableDataSource: (name: string) => Q.Promise<DataSource>; } export function dataSourceManagerFactory(options: DataSourceManagerOptions): DataSourceManager { var { dataSources, dataSourceStubFactory, druidRequester, dataSourceLoader, pageMustLoadTimeout, sourceListScan, sourceListRefreshOnLoad, sourceListRefreshInterval, sourceReintrospectOnLoad, sourceReintrospectInterval, log } = options; if (!pageMustLoadTimeout) pageMustLoadTimeout = 800; if (!sourceListScan) sourceListScan = 'auto'; if (sourceListScan !== 'disable' && sourceListScan !== 'auto') { throw new Error(`sourceListScan must be disable or auto is ('${sourceListScan}')`); } if (!dataSourceStubFactory) { dataSourceStubFactory = (source: string) => { return DataSource.fromJS({ name: makeUrlSafeName(source), engine: 'druid', source: source, refreshRule: RefreshRule.query().toJS() }); }; } if (!log) log = function() {}; var myDataSources: DataSource[] = dataSources || []; function findDataSource(name: string): DataSource { return helper.findByName(myDataSources, name); } function getQueryable(): DataSource[] { return myDataSources.filter((dataSource) => dataSource.isQueryable()); } // Updates the correct datasource (by name) in myDataSources function addOrUpdateDataSource(dataSource: DataSource): void { myDataSources = helper.overrideByName(myDataSources, dataSource); } function loadAndIntrospectDataSource(dataSource: DataSource): Q.Promise<DataSource> { return loadDataSource(dataSource) .then(introspectDataSource); } function loadDataSource(dataSource: DataSource): Q.Promise<DataSource> { return dataSourceLoader(dataSource) .then((loadedDataSource) => { addOrUpdateDataSource(loadedDataSource); return loadedDataSource; }) .catch((e): DataSource => { log(`Failed to load data source: '${dataSource.name}' because ${e.message}`); throw e; }); } function introspectDataSource(dataSource: DataSource, doLog = false): Q.Promise<DataSource> { return dataSource.introspect() .then((introspectedDataSource) => { if (introspectedDataSource !== dataSource) { if (doLog) log(`loaded new schema for ${dataSource.name}`); addOrUpdateDataSource(introspectedDataSource); var issues = introspectedDataSource.getIssues(); if (issues.length) { log(`Data source '${introspectedDataSource.name}' has the following issues:`); log('- ' + issues.join('\n- ') + '\n'); } } return introspectedDataSource; }) .catch((e): DataSource => { log(`Failed to introspect data source: '${dataSource.name}' because ${e.message}`); throw e; }); } function introspectDataSources(): Q.Promise<any> { return Q.allSettled(getQueryable().map((dataSource) => { return introspectDataSource(dataSource, true); })); } function loadDruidDataSources(): Q.Promise<any> { if (!druidRequester) return Q(null); return DruidExternal.getSourceList(druidRequester) .then((ds: string[]) => { if (!Array.isArray(ds)) throw new Error('invalid result from data source list'); var unknownDataSourceNames: string[] = []; var nonQueryableDataSources: DataSource[] = []; ds.forEach((d: string) => { var existingDataSources = myDataSources.filter((dataSource) => { return dataSource.engine === 'druid' && dataSource.source === d; }); if (existingDataSources.length === 0) { unknownDataSourceNames.push(d); } else { nonQueryableDataSources = nonQueryableDataSources.concat(existingDataSources.filter((dataSource) => { return !dataSource.isQueryable(); })); } }); nonQueryableDataSources = nonQueryableDataSources.concat(unknownDataSourceNames.map((source) => { var newDataSource = dataSourceStubFactory(source); log(`Adding Druid data source: '${source}'`); addOrUpdateDataSource(newDataSource); return newDataSource; })); // Nothing to do if (!nonQueryableDataSources.length) return Q(null); return Q.allSettled(nonQueryableDataSources.map(loadAndIntrospectDataSource)); }) .catch((e: Error) => { log(`Could not get druid source list: ${e.message}`); }); } // First concurrently introspect all the defined data sources var initialLoad: Q.Promise<any> = Q.allSettled(myDataSources.map(loadAndIntrospectDataSource)); // Then (if needed) scan for more data sources if (sourceListScan === 'auto' && druidRequester) { initialLoad = initialLoad.then(loadDruidDataSources); } // Then print out an update initialLoad.then(() => { var queryableDataSources = getQueryable(); log(`Initial load and introspection complete. Got ${myDataSources.length} data sources, ${queryableDataSources.length} queryable`); }); if (sourceListScan === 'auto' && druidRequester && sourceListRefreshInterval) { log(`Will refresh data source list every ${sourceListRefreshInterval}ms`); setInterval(loadDruidDataSources, sourceListRefreshInterval).unref(); } if (druidRequester && sourceReintrospectInterval) { log(`Will re-introspect data sources every ${sourceReintrospectInterval}ms`); setInterval(introspectDataSources, sourceReintrospectInterval).unref(); } // Periodically check if max time needs to be updated setInterval(() => { myDataSources.forEach((dataSource) => { if (dataSource.refreshRule.isQuery() && dataSource.shouldUpdateMaxTime()) { DataSource.updateMaxTime(dataSource).then((updatedDataSource) => { log(`Getting the latest MaxTime for '${updatedDataSource.name}'`); addOrUpdateDataSource(updatedDataSource); }); } }); }, 1000).unref(); function onLoadTasks(): Q.Promise<any> { var tasks = <Q.Promise<any>[]>[]; if (sourceListRefreshOnLoad) { tasks.push(loadDruidDataSources()); } if (sourceReintrospectOnLoad) { tasks.push(introspectDataSources()); } return Q.allSettled(tasks) .timeout(pageMustLoadTimeout) .catch(() => { log(`pageMustLoadTimeout (${pageMustLoadTimeout}) exceeded, loading anyways.`); return null; }); } return { getDataSources: () => { return initialLoad.then(() => { if (myDataSources.length && !sourceListRefreshOnLoad && !sourceReintrospectOnLoad) return myDataSources; // There are no data sources... lets try to load some: return onLoadTasks().then(() => { return myDataSources; // we tried }); }); }, getQueryableDataSources: () => { return initialLoad.then(() => { var queryableDataSources = getQueryable(); if (queryableDataSources.length && !sourceListRefreshOnLoad && !sourceReintrospectOnLoad) return queryableDataSources; // There are no data sources... lets try to load some: return onLoadTasks().then(() => { return getQueryable(); // we tried }); }); }, getQueryableDataSource: (name: string) => { return initialLoad.then(() => { var myDataSource = findDataSource(name); if (myDataSource) { if (myDataSource.isQueryable()) return myDataSource; return introspectDataSource(myDataSource).then(() => { var queryableDataSource = findDataSource(name); return (queryableDataSource && queryableDataSource.isQueryable()) ? queryableDataSource : null; }); } // There are no data sources... lets try to load some: return loadDruidDataSources().then(() => { var queryableDataSource = findDataSource(name); return (queryableDataSource && queryableDataSource.isQueryable()) ? queryableDataSource : null; }); }); } }; }