-
Notifications
You must be signed in to change notification settings - Fork 14k
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
feat(SIP-39): Async query support for charts #11499
Merged
Merged
Changes from 41 commits
Commits
Show all changes
50 commits
Select commit
Hold shift + click to select a range
67f7d5f
Generate JWT in Flask app
robdiciuccio 1e8c039
Refactor chart data API query logic, add JWT validation and async worker
robdiciuccio a50750b
Add redis stream implementation, refactoring
robdiciuccio 64fbfae
Add chart data cache endpoint, refactor QueryContext caching
robdiciuccio 219ce77
Merge branch master
robdiciuccio e377b31
Typing, linting, refactoring
robdiciuccio f7ac5b6
pytest fixes and openapi schema update
robdiciuccio 3a16283
Merge branch master
robdiciuccio 6805180
Enforce caching be configured for async query init
robdiciuccio d5eef4f
Async query processing for explore_json endpoint
robdiciuccio 467c6bb
Add /api/v1/async_event endpoint
robdiciuccio f867cd5
Async frontend for dashboards [WIP]
robdiciuccio 8111ea6
Chart async error message support, refactoring
robdiciuccio 3b41f16
Abstract asyncEvent middleware
robdiciuccio 49b6b52
Async chart loading for Explore
robdiciuccio 0e5c09e
Merge branch master
robdiciuccio e2dc30e
Pylint fixes
robdiciuccio 276c84e
asyncEvent middleware -> TypeScript, JS linting
robdiciuccio 4566c01
Chart data API: enforce forced_cache, add tests
robdiciuccio 9eba0c4
Add tests for explore_json endpoints
robdiciuccio d2e4529
Add test for chart data cache enpoint (no login)
robdiciuccio 0bd7d67
Consolidate set_and_log_cache and add STORE_CACHE_KEYS_IN_METADATA_DB…
robdiciuccio 4441459
Add tests for tasks/async_queries and address PR comments
robdiciuccio 5896799
Bypass non-JSON result formats for async queries
robdiciuccio 5999bf3
Add tests for redux middleware
robdiciuccio 17215a3
Merge branch master
robdiciuccio 27e4548
Remove debug statement
robdiciuccio 119cae7
Skip force_cached if no queryObj
robdiciuccio 9666408
SunburstViz: don't modify self.form_data
robdiciuccio d2ef464
Merge branch 'rd/async-queries-mvp' of github.com:preset-io/incubator…
robdiciuccio a8607c0
Merge branch master
robdiciuccio e40eb45
Fix failing annotation test
robdiciuccio f491789
Resolve merge/lint issues
robdiciuccio f01740e
Reduce polling delay
robdiciuccio 9e02017
Merge branch 'master' into rd/async-queries-mvp
robdiciuccio 838c526
Fix new getClientErrorObject reference
robdiciuccio f0de265
Fix flakey unit tests
robdiciuccio 066504f
/api/v1/async_event: increment redis stream ID, add tests
robdiciuccio d6c8a1d
PR feedback: refactoring, configuration
robdiciuccio fc5753a
Merge branch master
robdiciuccio 088a49c
Fixup: remove debugging
robdiciuccio c9b871e
Fix typescript errors due to redux upgrade
robdiciuccio 89925a5
Update UPDATING.md
robdiciuccio 0ad7234
Fix failing py tests
robdiciuccio c72b4c6
asyncEvent_spec.js -> asyncEvent_spec.ts
robdiciuccio 1fb7489
Refactor flakey Python 3.7 mock assertions
robdiciuccio 024da76
Fix another shared state issue in Py tests
robdiciuccio 887754b
Use 'sub' claim in JWT for user_id
robdiciuccio 601ec51
Refactor async middleware config
robdiciuccio df673cf
Fixup: restore FeatureFlag boolean type
robdiciuccio File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
261 changes: 261 additions & 0 deletions
261
superset-frontend/spec/javascripts/middleware/asyncEvent_spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,261 @@ | ||
/** | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this file be typescript, too? |
||
* Licensed to the Apache Software Foundation (ASF) under one | ||
* or more contributor license agreements. See the NOTICE file | ||
* distributed with this work for additional information | ||
* regarding copyright ownership. The ASF licenses this file | ||
* to you under the Apache License, Version 2.0 (the | ||
* "License"); you may not use this file except in compliance | ||
* with the License. You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
import fetchMock from 'fetch-mock'; | ||
import sinon from 'sinon'; | ||
import * as featureFlags from 'src/featureFlags'; | ||
import initAsyncEvents from 'src/middleware/asyncEvent'; | ||
|
||
jest.useFakeTimers(); | ||
|
||
describe('asyncEvent middleware', () => { | ||
const next = sinon.spy(); | ||
const state = { | ||
charts: { | ||
123: { | ||
id: 123, | ||
status: 'loading', | ||
asyncJobId: 'foo123', | ||
}, | ||
345: { | ||
id: 345, | ||
status: 'loading', | ||
asyncJobId: 'foo345', | ||
}, | ||
}, | ||
}; | ||
const events = [ | ||
{ | ||
status: 'done', | ||
result_url: '/api/v1/chart/data/cache-key-1', | ||
job_id: 'foo123', | ||
channel_id: '999', | ||
errors: [], | ||
}, | ||
{ | ||
status: 'done', | ||
result_url: '/api/v1/chart/data/cache-key-2', | ||
job_id: 'foo345', | ||
channel_id: '999', | ||
errors: [], | ||
}, | ||
]; | ||
const mockStore = { | ||
getState: () => state, | ||
dispatch: sinon.stub(), | ||
}; | ||
const action = { | ||
type: 'GENERIC_ACTION', | ||
}; | ||
const EVENTS_ENDPOINT = 'glob:*/api/v1/async_event/*'; | ||
const CACHED_DATA_ENDPOINT = 'glob:*/api/v1/chart/data/*'; | ||
let featureEnabledStub; | ||
let getFeatureStub; | ||
|
||
function setup() { | ||
const getPendingComponents = sinon.stub(); | ||
const successAction = sinon.spy(); | ||
const errorAction = sinon.spy(); | ||
const testCallback = sinon.stub(); | ||
const testCallbackPromise = sinon.stub(); | ||
testCallbackPromise.returns( | ||
new Promise(resolve => { | ||
testCallback.callsFake(resolve); | ||
}), | ||
); | ||
|
||
return { | ||
getPendingComponents, | ||
successAction, | ||
errorAction, | ||
testCallback, | ||
testCallbackPromise, | ||
}; | ||
} | ||
|
||
beforeEach(() => { | ||
fetchMock.get(EVENTS_ENDPOINT, { | ||
status: 200, | ||
body: { result: [] }, | ||
}); | ||
fetchMock.get(CACHED_DATA_ENDPOINT, { | ||
status: 200, | ||
body: { result: { some: 'data' } }, | ||
}); | ||
featureEnabledStub = sinon.stub(featureFlags, 'isFeatureEnabled'); | ||
featureEnabledStub.withArgs('GLOBAL_ASYNC_QUERIES').returns(true); | ||
getFeatureStub = sinon.stub(featureFlags, 'getFeatureFlag'); | ||
getFeatureStub | ||
.withArgs('GLOBAL_ASYNC_QUERIES_OPTIONS') | ||
.returns({ transport: 'polling', polling_delay: 250 }); | ||
}); | ||
afterEach(() => { | ||
fetchMock.reset(); | ||
next.resetHistory(); | ||
featureEnabledStub.restore(); | ||
getFeatureStub.restore(); | ||
}); | ||
afterAll(fetchMock.reset); | ||
|
||
it('should initialize and call next', () => { | ||
const { getPendingComponents, successAction, errorAction } = setup(); | ||
getPendingComponents.returns([]); | ||
const asyncEventMiddleware = initAsyncEvents({ | ||
getPendingComponents, | ||
successAction, | ||
errorAction, | ||
}); | ||
asyncEventMiddleware(mockStore)(next)(action); | ||
expect(next.callCount).toBe(1); | ||
}); | ||
|
||
it('should fetch events when there are pending components', () => { | ||
const { | ||
getPendingComponents, | ||
successAction, | ||
errorAction, | ||
testCallback, | ||
testCallbackPromise, | ||
} = setup(); | ||
getPendingComponents.returns(Object.values(state.charts)); | ||
const asyncEventMiddleware = initAsyncEvents({ | ||
getPendingComponents, | ||
successAction, | ||
errorAction, | ||
processEventsCallback: testCallback, | ||
}); | ||
|
||
asyncEventMiddleware(mockStore)(next)(action); | ||
|
||
return testCallbackPromise().then(() => { | ||
expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(1); | ||
}); | ||
}); | ||
|
||
it('should fetch cached when there are successful events', () => { | ||
const { | ||
getPendingComponents, | ||
successAction, | ||
errorAction, | ||
testCallback, | ||
testCallbackPromise, | ||
} = setup(); | ||
fetchMock.reset(); | ||
fetchMock.get(EVENTS_ENDPOINT, { | ||
status: 200, | ||
body: { result: events }, | ||
}); | ||
fetchMock.get(CACHED_DATA_ENDPOINT, { | ||
status: 200, | ||
body: { result: { some: 'data' } }, | ||
}); | ||
getPendingComponents.returns(Object.values(state.charts)); | ||
const asyncEventMiddleware = initAsyncEvents({ | ||
getPendingComponents, | ||
successAction, | ||
errorAction, | ||
processEventsCallback: testCallback, | ||
}); | ||
|
||
asyncEventMiddleware(mockStore)(next)(action); | ||
|
||
return testCallbackPromise().then(() => { | ||
expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(1); | ||
expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(2); | ||
expect(successAction.callCount).toBe(2); | ||
}); | ||
}); | ||
|
||
it('should call errorAction for cache fetch error responses', () => { | ||
const { | ||
getPendingComponents, | ||
successAction, | ||
errorAction, | ||
testCallback, | ||
testCallbackPromise, | ||
} = setup(); | ||
fetchMock.reset(); | ||
fetchMock.get(EVENTS_ENDPOINT, { | ||
status: 200, | ||
body: { result: events }, | ||
}); | ||
fetchMock.get(CACHED_DATA_ENDPOINT, { | ||
status: 400, | ||
body: { errors: ['error'] }, | ||
}); | ||
getPendingComponents.returns(Object.values(state.charts)); | ||
const asyncEventMiddleware = initAsyncEvents({ | ||
getPendingComponents, | ||
successAction, | ||
errorAction, | ||
processEventsCallback: testCallback, | ||
}); | ||
|
||
asyncEventMiddleware(mockStore)(next)(action); | ||
|
||
return testCallbackPromise().then(() => { | ||
expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(1); | ||
expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(2); | ||
expect(errorAction.callCount).toBe(2); | ||
}); | ||
}); | ||
|
||
it('should handle event fetching error responses', () => { | ||
const { | ||
getPendingComponents, | ||
successAction, | ||
errorAction, | ||
testCallback, | ||
testCallbackPromise, | ||
} = setup(); | ||
fetchMock.reset(); | ||
fetchMock.get(EVENTS_ENDPOINT, { | ||
status: 400, | ||
body: { message: 'error' }, | ||
}); | ||
getPendingComponents.returns(Object.values(state.charts)); | ||
const asyncEventMiddleware = initAsyncEvents({ | ||
getPendingComponents, | ||
successAction, | ||
errorAction, | ||
processEventsCallback: testCallback, | ||
}); | ||
|
||
asyncEventMiddleware(mockStore)(next)(action); | ||
|
||
return testCallbackPromise().then(() => { | ||
expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(1); | ||
}); | ||
}); | ||
|
||
it('should not fetch events when async queries are disabled', () => { | ||
featureEnabledStub.restore(); | ||
featureEnabledStub = sinon.stub(featureFlags, 'isFeatureEnabled'); | ||
featureEnabledStub.withArgs('GLOBAL_ASYNC_QUERIES').returns(false); | ||
const { getPendingComponents, successAction, errorAction } = setup(); | ||
getPendingComponents.returns(Object.values(state.charts)); | ||
const asyncEventMiddleware = initAsyncEvents({ | ||
getPendingComponents, | ||
successAction, | ||
errorAction, | ||
}); | ||
|
||
asyncEventMiddleware(mockStore)(next)(action); | ||
expect(getPendingComponents.called).toBe(false); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't actually have to specify the full path.
should also work.