virool-pivot
Version:
A web-based exploratory visualization UI for Druid.io
270 lines (224 loc) • 9.18 kB
text/typescript
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;
});
});
}
};
}