-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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(cache-cell-results): added cell result caching to reduce cell configuration querying on loads #18581
Conversation
cb0a5ae
to
27e4c32
Compare
ui/src/data/actions/index.ts
Outdated
| ReturnType<typeof resetCachedQueryResults> | ||
| ReturnType<typeof setQueryResultsByQueryID> | ||
|
||
export const hashCode = s => |
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.
it's usually a good idea to paste the link you copy/pasted internet code form so people can get some context on it, and in the case of bugs, go right to the source.
ui/src/data/reducers/index.ts
Outdated
} | ||
|
||
return state | ||
} |
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.
is this file necessary? why can't the query results be stored in the time machine reducer?
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.
I had originally set the data in the time machine, but it became a little tricky accessing the data since timeMachine is partitioned into segmented subsets. The thinking with this feature is that we might be also benefit from this caching across the platform in the future, so if the same query is run in data-explorer and a dashboard cell, we'd be able to cache those results and reduce querying
@@ -344,6 +349,7 @@ const mstp = (state: AppState, props: OwnProps): StateProps => { | |||
|
|||
const mdtp: DispatchProps = { | |||
notify: notifyAction, | |||
setQueryResults: setQueryResultsByQueryID, |
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.
what's the idea behind aliasing this? it's really confusing - there's already a method being exported from another file called setQueryResults
. this really confounds the ability to see where setQueryResults
is used - this represents a false positive of that.
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.
Fair point. I think aliasing helps differentiate between the imported action vs the one that's accessible in the props. Without aliasing, I find that my workflow is:
- Import the action
- Try and call the action directly within the component
- Get confused for a bit, do some debugging, then realize the err of my ways
Having said that, I'm fine with not aliasing if it makes things clearer
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.
for differentiate between the imported action vs the one that's accessible in the props
we sometimes use setX
vs onSetX
I had personally set a bad example of this in this file. :(
ui/src/views/actions/thunks.ts
Outdated
dispatch(executeQueries()) | ||
} catch (error) { | ||
// if the files don't exist in the cache, we want to execute the query | ||
console.error(error) |
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.
if i'm understanding this correctly, this logs an error on cache misses? seems noisy - if that's the case i think we should just swallow this error and not log it.
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.
Good point.
|
||
@ErrorHandling | ||
class DashboardPage extends Component<Props> { | ||
public componentWillUnmount() { | ||
this.props.resetCachedQueryResults() |
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.
👍 thinking about cache invalidation
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.
Can't take any credit for it, it was all @ebb-tide 's idea
ui/src/types/stores.ts
Outdated
@@ -40,6 +43,7 @@ export interface AppState { | |||
} | |||
currentPage: CurrentPage | |||
currentDashboard: CurrentDashboardState | |||
data: DataState |
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.
i'm going to be blunt here: data
is a really poor name for a reducer - all reducers deal in data. i'm not sure if this reducer needs to exist, but if it does, what's wrong with calling it the cache
reducer or the queryCache
reducer?
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.
Those are really great names, I'll switch them up
|
||
@ErrorHandling | ||
class DashboardPage extends Component<Props> { | ||
public componentWillUnmount() { | ||
this.props.resetCachedQueryResults() |
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.
👍
ui/src/data/reducers/index.ts
Outdated
} | ||
|
||
case 'RESET_CACHED_QUERY_RESULTS': { | ||
return initialState |
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.
let's return a brand new object here just in case.
ui/src/store/configureStore.ts
Outdated
@@ -73,6 +74,7 @@ export const rootReducer = combineReducers<ReducerState>({ | |||
}), | |||
currentPage: currentPageReducer, | |||
currentDashboard: currentDashboardReducer, | |||
data: dataReducer, |
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.
maybe call this queryCache
and queryCacheReducer
?. data can mean sooo many things... :/
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.
totally. Just seeing this. how about dashboardQueryCache? or are we thinking something a little less specific?
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.
I like queryCache, I can't tell if we will only use this in dashboards or not.
ui/src/views/actions/thunks.ts
Outdated
const state = getState() | ||
const {view} = getActiveTimeMachine(state) | ||
const queries = view.properties.queries.filter(({text}) => !!text.trim()) | ||
const queryID = get(queries, '[0].text', '') |
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.
The function name setQueryResultsByQueryID
suggests that you will be hashing first and then passing queryID rather than queryText to the function. I think you should be hashing on this line.
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.
Definitely. I was thinking that as I was walking through this time around. I'll go ahead and import it in and hash it here
ui/src/views/actions/thunks.ts
Outdated
const {view} = getActiveTimeMachine(state) | ||
const queries = view.properties.queries.filter(({text}) => !!text.trim()) | ||
const queryID = get(queries, '[0].text', '') | ||
if (queryID) { |
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.
This should be where you lookup to see whether the queryID exists in your cache.
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.
in TimeSeries- we setQueryResults, here we should read it back from queryResults.
so if state.queryResults.Query.queryResultsByQueryID[queryID]
exists we should use it in the visualization, and not executeQueries. (you might need to look into how to use in the visualization)
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.
I think there might be a misunderstanding stemming from naming two different functions the same thing, since this dispatch should be dispatching the action above:
and not the action to set the data
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.
yes, that is pretty confusing :) I don't think we need a separate setQueryResultsByQueryID
function here, just roll what it does into this action.
ui/src/data/actions/index.ts
Outdated
export const hashCode = s => | ||
s.split('').reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0) | ||
|
||
export const setQueryResultsByQueryID = (queryID: string, files: string[]) => |
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.
it's really confusing when the variable is called queryID
but you are actually passing in the queryText, and you will convert it into the ID inside the function. Could you change the name of this function to setQueryResultsForQuery(queryText: string, files: string[]))
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.
Really great point. I ended up importing the hashCode
function into the TimeSeries
component to set the ID there, so now it's actually passing the ID in
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.
sorry I'm not done!
ui/src/data/reducers/index.ts
Outdated
case 'SET_QUERY_RESULTS_BY_QUERY': { | ||
return produce(state, draftState => { | ||
const {queryID, files} = action | ||
if (queryID && files.length) { |
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.
I think we should be checking these conditions before firing SET_QUERY_RESULTS_BY_QUERY
action, and then we can rely on them being here.
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.
Sounds good.
5a61d99
to
57c49a0
Compare
const { | ||
alertBuilder: {id: checkID}, | ||
} = state | ||
|
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.
Moved this down so it would be place a little closer to the source of where checkID
is being used
ui/src/views/actions/thunks.ts
Outdated
): Promise<void> => { | ||
try { | ||
const state = getState() | ||
const files = state.queryCache.queryResultsByQueryID[hashCode(queryID)] |
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 are hashing queryID before sending to this function. so you shouldn't be hashing here.
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.
Hmmm, I'm not sure if i am, but I definitely should be since it's a little misleading at this point
@@ -271,6 +280,12 @@ class TimeSeries extends Component<Props & WithRouterProps, State> { | |||
|
|||
this.pendingReload = false | |||
|
|||
const queryText = queries[0].text |
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.
This will not work if there is more than one query in queries
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.
I was wondering about that. I've never seen a situation where more than one query is being sent, but is that a possibility? If so, should we cache each one separately?
ui/src/views/actions/thunks.ts
Outdated
timeMachineID: TimeMachineID | ||
) => (dispatch, getState: GetState): Promise<void> => { | ||
try { | ||
dispatch(getViewForTimeMachine(dashboardID, cellID, timeMachineID)) |
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.
I'm worried about race conditions here. We can not await the results of a thunk, we just dispatch it, and so if we are depending on its results as we are... on line 151, we're hoping that getViewForTimeMachine has done its thing already and we can get the view. The solutions is to integrate the getViewForTimeMachine
and setQueryResultsForCell
functions in to one.
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.
I hear what you're saying, it's definitely a valid concern, but it seems like it might be overstepping the intent of the function. I'm curious as to how moving the dispatches within the scope of setQueryResultsForCell
would prevent a race condition?
593db7b
to
cec9b0d
Compare
ui/src/views/actions/thunks.ts
Outdated
dispatch(executeQueries()) | ||
} catch (error) { | ||
// if the files don't exist in the cache, we want to execute the query | ||
dispatch(executeQueries()) |
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.
I don't think there is an error here that is relevant. We are already checking if files don't exist on line 132, and executing queries if they don't. on line 136
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.
that's a really good point that I didn't consider. Should we just remove this from the try/catch?
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.
I think so, yeah.
view, | ||
}) => { | ||
useEffect(() => { | ||
// TODO split this up into "loadView" "setActiveTimeMachine" | ||
// and something to tell the component to pull from the context | ||
// of the dashboardID | ||
getViewForTimeMachine(dashboardID, cellID, 'veo') | ||
setQueryResultsForCell(dashboardID, cellID, 'veo') |
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.
ok this is a nit, but this function name is a lot less descriptive to me. can we switch it to something that has a reference to view? maybe getViewAndResultsForVEO
?
638733e
to
ab3dfd3
Compare
spoke offline and addressed the issues
1b09f53
to
2179681
Compare
…ved as a means of caching
… separated some of the view thunk logic to. Need to add more explicit typing and need to integrate cache expiration logic
…nt unmounts. Also separate thunk concerns to default query execution when files are undefined
Adding context to where the hashing function was found Renaming the data src to queryCache Renaming the imported function in TimeSeries Renaming some of the input parameters to be a little more relevant
…ased on the query text and renamed var to align with realistic output
…t in the output file
…ved as a means of caching
… separated some of the view thunk logic to. Need to add more explicit typing and need to integrate cache expiration logic
…nt unmounts. Also separate thunk concerns to default query execution when files are undefined
Adding context to where the hashing function was found Renaming the data src to queryCache Renaming the imported function in TimeSeries Renaming some of the input parameters to be a little more relevant
Co-authored-by: Deniz Kusefoglu <[email protected]>
…ctionality of getViewAndResultsForVeo to eliminate possible race condition
2179681
to
655d5de
Compare
Closes #18401
Problem
Dashboard cells are queried once when they are initially loaded, then again when the cell is opened for configuration
Solution
Implemented a reducer-based cache to cache dashboard cell queries and their results based on a hashed query index. Now, if a cell is opened for configuration, the action will check to see if a result exists for that hashed query, otherwise the query will execute. Once the dashboard unmounts, the query is cached.