Merge pull request #5252 from spalger/fix/segmentedIndexFetchOrder

Fix/segmented index fetch order
This commit is contained in:
Spencer 2015-11-04 17:29:07 -06:00
commit 1436b7e05b
6 changed files with 187 additions and 60 deletions

View file

@ -0,0 +1,62 @@
describe('ui/courier/fetch/request/segmented/_createQueue', () => {
const sinon = require('auto-release-sinon');
const expect = require('expect.js');
const ngMock = require('ngMock');
let Promise;
let $rootScope;
let SegmentedReq;
let MockSource;
require('testUtils/noDigestPromises').activateForSuite();
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject((Private, $injector) => {
Promise = $injector.get('Promise');
$rootScope = $injector.get('$rootScope');
SegmentedReq = Private(require('ui/courier/fetch/request/segmented'));
const StubbedSearchSourceProvider = require('fixtures/stubbed_search_source');
MockSource = class {
constructor() {
return $injector.invoke(StubbedSearchSourceProvider);
}
};
}));
it('manages the req._queueCreated flag', async function () {
const req = new SegmentedReq(new MockSource());
req._queueCreated = null;
var promise = req._createQueue();
expect(req._queueCreated).to.be(false);
await promise;
expect(req._queueCreated).to.be(true);
});
it('relies on indexPattern.toIndexList to generate queue', async function () {
const source = new MockSource();
const ip = source.get('index');
const indices = [1,2,3];
sinon.stub(ip, 'toIndexList').returns(Promise.resolve(indices));
const req = new SegmentedReq(source);
const output = await req._createQueue();
expect(output).to.equal(indices);
});
it('tells the index pattern its direction', async function () {
const source = new MockSource();
const ip = source.get('index');
const req = new SegmentedReq(source);
sinon.stub(ip, 'toIndexList').returns(Promise.resolve([1,2,3]));
req.setDirection('asc');
await req._createQueue();
expect(ip.toIndexList.lastCall.args[2]).to.be('asc');
req.setDirection('desc');
await req._createQueue();
expect(ip.toIndexList.lastCall.args[2]).to.be('desc');
});
});

View file

@ -167,12 +167,12 @@ define(function (require) {
SegmentedReq.prototype._createQueue = function () {
var self = this;
var timeBounds = timefilter.getBounds();
var indexPattern = this.source.get('index');
var indexPattern = self.source.get('index');
self._queueCreated = false;
return indexPattern.toIndexList(timeBounds.min, timeBounds.max)
return indexPattern.toIndexList(timeBounds.min, timeBounds.max, self._direction)
.then(function (queue) {
if (!_.isArray(queue)) queue = [queue];
if (self._direction === 'desc') queue = queue.reverse();
self._queue = queue;
self._queueCreated = true;

View file

@ -313,6 +313,31 @@ describe('index pattern', function () {
expect(indexList[0]).to.equal('foo');
expect(indexList[1]).to.equal('bar');
});
context('with sort order', function () {
require('testUtils/noDigestPromises').activateForSuite();
context('undefined', function () {
it('provides the index list in tact', async function () {
const indexList = await indexPattern.toIndexList();
expect(indexList).to.eql(['foo', 'bar']);
});
});
context('asc', function () {
it('provides the index list in tact', async function () {
const indexList = await indexPattern.toIndexList(1, 2, 'asc');
expect(indexList).to.eql(['foo', 'bar']);
});
});
context('desc', function () {
it('reverses the index list', async function () {
const indexList = await indexPattern.toIndexList(1, 2, 'desc');
expect(indexList).to.eql(['bar', 'foo']);
});
});
});
});
context('when index pattern is a time-base wildcard', function () {
@ -322,13 +347,13 @@ describe('index pattern', function () {
sinon.stub(indexPattern, 'isWildcard').returns(true);
});
it('invokes calculateIndices with given start/stop times', function () {
indexPattern.toIndexList(1, 2);
it('invokes calculateIndices with given start/stop times and sortOrder', function () {
indexPattern.toIndexList(1, 2, 'sortOrder');
$rootScope.$apply();
var id = indexPattern.id;
var field = indexPattern.timeFieldName;
expect(calculateIndices.calledWith(id, field, 1, 2)).to.be(true);
expect(calculateIndices.calledWith(id, field, 1, 2, 'sortOrder')).to.be(true);
});
it('is fulfilled by the result of calculateIndices', function () {
var indexList;

View file

@ -7,37 +7,49 @@ describe('ui/index_patterns/_calculate_indices', () => {
let Promise;
let $rootScope;
let calculateIndices;
let error;
let es;
let response;
let transportRequest;
let config;
let constraints;
beforeEach(ngMock.module('kibana', ($provide) => {
error = undefined;
response = { indices: { 'mock-*': 'irrelevant, is ignored' } };
transportRequest = sinon.spy((options, fn) => fn(error, response));
$provide.value('es', _.set({}, 'transport.request', transportRequest));
response = {
indices: {
'mock-*': 'irrelevant, is ignored'
}
};
$provide.service('es', function (Promise) {
return {
fieldStats: sinon.spy(function () {
return Promise.resolve(response);
})
};
});
}));
beforeEach(ngMock.inject((Private, $injector) => {
$rootScope = $injector.get('$rootScope');
es = $injector.get('es');
Promise = $injector.get('Promise');
calculateIndices = Private(require('ui/index_patterns/_calculate_indices'));
}));
function run({ start = undefined, stop = undefined } = {}) {
calculateIndices('wat-*-no', '@something', start, stop);
$rootScope.$apply();
config = _.first(es.fieldStats.firstCall.args);
constraints = config.body.index_constraints;
}
describe('transport configuration', () => {
it('is POST', () => {
it('uses pattern path for indec', () => {
run();
expect(config.method).to.equal('POST');
});
it('uses pattern path for _field_stats', () => {
run();
expect(config.path).to.equal('/wat-*-no/_field_stats');
expect(config.index).to.equal('wat-*-no');
});
it('has level indices', () => {
run();
expect(config.query.level).to.equal('indices');
expect(config.level).to.equal('indices');
});
it('includes time field', () => {
run();
@ -69,30 +81,55 @@ describe('ui/index_patterns/_calculate_indices', () => {
});
});
describe('returned promise', () => {
it('is rejected by transport errors', () => {
error = 'something';
describe('response sorting', function () {
require('testUtils/noDigestPromises').activateForSuite();
let reason;
calculateIndices('one', 'two').then(null, val => reason = val);
$rootScope.$apply();
context('when no sorting direction given', function () {
it('returns the indices in the order that elasticsearch sends them', function () {
response = {
indices: {
c: { fields: { time: {} } },
a: { fields: { time: {} } },
b: { fields: { time: {} } },
}
};
expect(reason).to.equal(error);
return calculateIndices('*', 'time', null, null).then(function (resp) {
expect(resp).to.eql(['c', 'a', 'b']);
});
});
});
it('is fulfilled by array of indices in successful response', () => {
let indices;
calculateIndices('one', 'two').then(val => indices = val);
$rootScope.$apply();
context('when sorting desc', function () {
it('returns the indices in max_value order', function () {
response = {
indices: {
c: { fields: { time: { max_value: 10 } } },
a: { fields: { time: { max_value: 15 } } },
b: { fields: { time: { max_value: 1 } } },
}
};
expect(_.first(indices)).to.equal('mock-*');
return calculateIndices('*', 'time', null, null, 'desc').then(function (resp) {
expect(resp).to.eql(['a', 'c', 'b']);
});
});
});
context('when sorting asc', function () {
it('returns the indices in min_value order', function () {
response = {
indices: {
c: { fields: { time: { max_value: 10, min_value: 9 } } },
a: { fields: { time: { max_value: 15, min_value: 5 } } },
b: { fields: { time: { max_value: 1, min_value: 0 } } },
}
};
return calculateIndices('*', 'time', null, null, 'asc').then(function (resp) {
expect(resp).to.eql(['b', 'a', 'c']);
});
});
});
});
function run({ start = undefined, stop = undefined } = {}) {
calculateIndices('wat-*-no', '@something', start, stop);
$rootScope.$apply();
config = _.first(transportRequest.firstCall.args);
constraints = config.body.index_constraints;
}
});

View file

@ -7,14 +7,14 @@ define(function (require) {
// Uses the field stats api to determine the names of indices that need to
// be queried against that match the given pattern and fall within the
// given time range
function calculateIndices(...args) {
const options = compileOptions(...args);
return sendRequest(options);
function calculateIndices(pattern, timeFieldName, start, stop, sortDirection) {
return getFieldStats(pattern, timeFieldName, start, stop)
.then(resp => sortIndexStats(resp, timeFieldName, sortDirection));
};
// creates the configuration hash that must be passed to the elasticsearch
// client
function compileOptions(pattern, timeFieldName, start, stop) {
function getFieldStats(pattern, timeFieldName, start, stop) {
const constraints = {};
if (start) {
constraints.max_value = { gte: moment(start).valueOf() };
@ -23,30 +23,32 @@ define(function (require) {
constraints.min_value = { lte: moment(stop).valueOf() };
}
return {
method: 'POST',
path: `/${pattern}/_field_stats`,
query: {
level: 'indices'
},
return es.fieldStats({
index: pattern,
level: 'indices',
body: {
fields: [ timeFieldName ],
index_constraints: {
[timeFieldName]: constraints
}
}
};
});
}
// executes a request to elasticsearch with the given configuration hash
function sendRequest(options) {
return new Promise(function (resolve, reject) {
es.transport.request(options, function (err, response) {
if (err) return reject(err);
const indices = _.map(response.indices, (info, index) => index);
resolve(indices);
});
});
function sortIndexStats(resp, timeFieldName, sortDirection) {
if (!sortDirection) return _.keys(resp.indices);
// FIXME: Once https://github.com/elastic/elasticsearch/issues/14404 is closed
// this should be sorting based on the sortable value of a field.
const edgeKey = sortDirection === 'desc' ? 'max_value' : 'min_value';
return _(resp.indices)
.map((stats, index) => (
{ index, edge: stats.fields[timeFieldName][edgeKey] }
))
.sortByOrder(['edge'], [sortDirection])
.pluck('index')
.value();
}
return calculateIndices;

View file

@ -176,15 +176,16 @@ define(function (require) {
return this.intervalName && _.find(intervals, { name: this.intervalName });
};
self.toIndexList = function (start, stop) {
self.toIndexList = function (start, stop, sortDirection) {
return new Promise(function (resolve) {
var indexList;
var interval = self.getInterval();
if (interval) {
indexList = intervals.toIndexList(self.id, interval, start, stop);
if (sortDirection === 'desc') indexList = indexList.reverse();
} else if (self.isWildcard() && self.hasTimeField()) {
indexList = calculateIndices(self.id, self.timeFieldName, start, stop);
indexList = calculateIndices(self.id, self.timeFieldName, start, stop, sortDirection);
} else {
indexList = self.id;
}