throttle loading outcomes on student grades page
fixes TALLY-228 test plan: * Smoke test student grades page with many outcomes * Jenkins oughtta be enough Change-Id: Idc66f8b2cccea29da3a5641eb9ba0776ea623a2f Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/216867 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Tested-by: Jenkins QA-Review: Gary Mei <gmei@instructure.com> Product-Review: Spencer Olson <solson@instructure.com> Reviewed-by: Gary Mei <gmei@instructure.com> Reviewed-by: Spencer Olson <solson@instructure.com>
This commit is contained in:
parent
ba650e5b96
commit
17cb63df31
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const ACTIVE_REQUEST_LIMIT = 12 // naive limit
|
||||
|
||||
export default class NaiveFetchDispatch {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
activeRequestLimit: ACTIVE_REQUEST_LIMIT,
|
||||
...options
|
||||
}
|
||||
this.requests = []
|
||||
}
|
||||
|
||||
get activeRequestCount() {
|
||||
// Return the count of active requests currently in the queue.
|
||||
return this.requests.filter(request => request.active).length
|
||||
}
|
||||
|
||||
get nextPendingRequest() {
|
||||
// Return the first request in the queue which has not been started.
|
||||
return this.requests.find(request => !request.active)
|
||||
}
|
||||
|
||||
addRequest(request) {
|
||||
this.requests.push(request)
|
||||
this.fillQueue()
|
||||
}
|
||||
|
||||
clearRequest(request) {
|
||||
this.requests = this.requests.filter(r => r !== request)
|
||||
this.fillQueue()
|
||||
}
|
||||
|
||||
fillQueue() {
|
||||
let nextRequest = this.nextPendingRequest
|
||||
while (nextRequest != null && this.activeRequestCount < this.options.activeRequestLimit) {
|
||||
nextRequest.start()
|
||||
nextRequest = this.nextPendingRequest
|
||||
}
|
||||
}
|
||||
|
||||
fetch(...args) {
|
||||
const request = {
|
||||
active: false
|
||||
}
|
||||
|
||||
request.promise = new Promise((resolve, reject) => {
|
||||
request.resolve = resolve
|
||||
request.reject = reject
|
||||
})
|
||||
|
||||
request.start = () => {
|
||||
/*
|
||||
* Update the request as "active" so that it is counted as an active
|
||||
* request in the queue and is not restarted when filling the queue.
|
||||
*/
|
||||
request.active = true
|
||||
|
||||
/* eslint-disable promise/catch-or-return */
|
||||
fetch(...args)
|
||||
.then(request.resolve)
|
||||
.catch(request.reject)
|
||||
.finally(() => {
|
||||
this.clearRequest(request)
|
||||
})
|
||||
/* eslint-enable promise/catch-or-return */
|
||||
}
|
||||
|
||||
this.addRequest(request)
|
||||
|
||||
return request.promise
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import fetchMock from 'fetch-mock'
|
||||
|
||||
import NaiveFetchDispatch from '../NaiveFetchDispatch'
|
||||
|
||||
describe('Outcomes > IndividualStudentMastery > NaiveFetchDispatch', () => {
|
||||
const URL = 'http://localhost/example'
|
||||
|
||||
let dispatch
|
||||
|
||||
beforeEach(() => {
|
||||
dispatch = new NaiveFetchDispatch({activeRequestLimit: 2})
|
||||
})
|
||||
|
||||
describe('#fetch()', () => {
|
||||
let exampleData
|
||||
|
||||
function resourceUrl(resourceIndex) {
|
||||
return `${URL}/?index=${resourceIndex}`
|
||||
}
|
||||
|
||||
function stageRequests(resourceCount) {
|
||||
for (let resourceIndex = 1; resourceIndex <= resourceCount; resourceIndex++) {
|
||||
exampleData[resourceIndex] = {resourceIndex}
|
||||
fetchMock.mock(resourceUrl(resourceIndex), exampleData[resourceIndex])
|
||||
}
|
||||
}
|
||||
|
||||
function fetch(resourceIndex) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dispatch
|
||||
.fetch(resourceUrl(resourceIndex))
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
exampleData = {}
|
||||
stageRequests(4)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.restore()
|
||||
})
|
||||
|
||||
it('sends a request for the resource', async () => {
|
||||
await fetch(1)
|
||||
expect(fetchMock.calls(url => url.match(URL))).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('resolves with the data from the request', async () => {
|
||||
const response = await fetch(1)
|
||||
expect(response.body).toEqual(JSON.stringify(exampleData[1]))
|
||||
})
|
||||
|
||||
it('resolves when flooded with requests', async () => {
|
||||
const requests = [1, 2, 3, 4].map(fetch)
|
||||
await Promise.all(requests)
|
||||
expect(fetchMock.calls(url => url.match(URL))).toHaveLength(4) // 4 resources
|
||||
})
|
||||
})
|
||||
})
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
import fetchMock from 'fetch-mock'
|
||||
import fetchOutcomes, {fetchUrl} from '../fetchOutcomes'
|
||||
import NaiveFetchDispatch from '../NaiveFetchDispatch'
|
||||
|
||||
describe('fetchOutcomes', () => {
|
||||
afterEach(() => {
|
||||
|
@ -206,6 +207,12 @@ describe('fetchOutcomes', () => {
|
|||
})
|
||||
|
||||
describe('fetchUrl', () => {
|
||||
let dispatch
|
||||
|
||||
beforeEach(() => {
|
||||
dispatch = new NaiveFetchDispatch({activeRequestLimit: 2})
|
||||
})
|
||||
|
||||
const mockRequests = (first, second, third) => {
|
||||
fetchMock.mock('/first', {
|
||||
body: first,
|
||||
|
@ -233,40 +240,36 @@ describe('fetchOutcomes', () => {
|
|||
}
|
||||
}
|
||||
|
||||
it('combines result arrays', done => {
|
||||
it('combines result arrays', () => {
|
||||
mockRequests([1, 'hello world', {foo: 'bar'}], [2, 'goodbye', {baz: 'bat'}])
|
||||
fetchUrl('/first').then(resp => {
|
||||
return fetchUrl('/first', dispatch).then(resp => {
|
||||
expect(resp).toEqual([1, 'hello world', {foo: 'bar'}, 2, 'goodbye', {baz: 'bat'}])
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('combines result objects', done => {
|
||||
it('combines result objects', () => {
|
||||
mockRequests({a: 'b', c: ['d', 'e', 'f']}, {g: 'h', c: ['i', 'j', 'k']})
|
||||
fetchUrl('/first').then(resp => {
|
||||
return fetchUrl('/first', dispatch).then(resp => {
|
||||
expect(resp).toEqual({a: 'b', c: ['d', 'e', 'f', 'i', 'j', 'k'], g: 'h'})
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('handles three requests', done => {
|
||||
it('handles three requests', () => {
|
||||
mockRequests({a: 'b', c: ['d', 'e', 'f']}, {g: 'h', c: ['i', 'j', 'k']}, {a: 'x'})
|
||||
fetchUrl('/first').then(resp => {
|
||||
return fetchUrl('/first', dispatch).then(resp => {
|
||||
expect(resp).toEqual({a: 'x', c: ['d', 'e', 'f', 'i', 'j', 'k'], g: 'h'})
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('handles deeply nested objects', done => {
|
||||
it('handles deeply nested objects', () => {
|
||||
mockRequests(
|
||||
{a: {b: {c: {d: ['e', 'f'], g: ['h', 'i'], j: 'k'}}}},
|
||||
{a: {b: {c: {d: ['e2', 'f2'], g: ['h2', 'i2'], j: 'k2'}}}}
|
||||
)
|
||||
fetchUrl('/first').then(resp => {
|
||||
return fetchUrl('/first', dispatch).then(resp => {
|
||||
expect(resp).toEqual({
|
||||
a: {b: {c: {d: ['e', 'f', 'e2', 'f2'], g: ['h', 'i', 'h2', 'i2'], j: 'k2'}}}
|
||||
})
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import _ from 'lodash'
|
||||
import uuid from 'uuid'
|
||||
import parseLinkHeader from 'parse-link-header'
|
||||
import NaiveFetchDispatch from './NaiveFetchDispatch'
|
||||
|
||||
const deepMerge = (lhs, rhs) => {
|
||||
if (lhs === undefined || lhs === null) {
|
||||
|
@ -37,20 +38,25 @@ const combine = (promiseOfJson1, promiseOfJson2) =>
|
|||
|
||||
const parse = response => response.text().then(text => JSON.parse(text.replace('while(1);', '')))
|
||||
|
||||
export const fetchUrl = url =>
|
||||
fetch(url, {
|
||||
credentials: 'include'
|
||||
}).then(response => {
|
||||
const linkHeader = response.headers.get('link')
|
||||
const next = linkHeader ? parseLinkHeader(linkHeader).next : null
|
||||
if (next) {
|
||||
return combine(parse(response), fetchUrl(next.url))
|
||||
} else {
|
||||
return parse(response)
|
||||
}
|
||||
})
|
||||
export function fetchUrl(url, dispatch) {
|
||||
return dispatch
|
||||
.fetch(url, {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => {
|
||||
const linkHeader = response.headers.get('link')
|
||||
const next = linkHeader ? parseLinkHeader(linkHeader).next : null
|
||||
if (next) {
|
||||
return combine(parse(response), fetchUrl(next.url, dispatch))
|
||||
} else {
|
||||
return parse(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const fetchOutcomes = (courseId, studentId) => {
|
||||
const dispatch = new NaiveFetchDispatch()
|
||||
|
||||
let outcomeGroups
|
||||
let outcomeLinks
|
||||
let outcomeRollups
|
||||
|
@ -58,11 +64,19 @@ const fetchOutcomes = (courseId, studentId) => {
|
|||
let outcomeResultsByOutcomeId
|
||||
let assignmentsByAssignmentId
|
||||
|
||||
function fetchWithDispatch(url) {
|
||||
return fetchUrl(url, dispatch)
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
fetchUrl(`/api/v1/courses/${courseId}/outcome_groups?per_page=100`),
|
||||
fetchUrl(`/api/v1/courses/${courseId}/outcome_group_links?outcome_style=full&per_page=100`),
|
||||
fetchUrl(`/api/v1/courses/${courseId}/outcome_rollups?user_ids[]=${studentId}&per_page=100`),
|
||||
fetchUrl(`/api/v1/courses/${courseId}/outcome_alignments?student_id=${studentId}`)
|
||||
fetchWithDispatch(`/api/v1/courses/${courseId}/outcome_groups?per_page=100`),
|
||||
fetchWithDispatch(
|
||||
`/api/v1/courses/${courseId}/outcome_group_links?outcome_style=full&per_page=100`
|
||||
),
|
||||
fetchWithDispatch(
|
||||
`/api/v1/courses/${courseId}/outcome_rollups?user_ids[]=${studentId}&per_page=100`
|
||||
),
|
||||
fetchWithDispatch(`/api/v1/courses/${courseId}/outcome_alignments?student_id=${studentId}`)
|
||||
])
|
||||
.then(([groups, links, rollups, alignments]) => {
|
||||
outcomeGroups = groups
|
||||
|
@ -73,7 +87,7 @@ const fetchOutcomes = (courseId, studentId) => {
|
|||
.then(() =>
|
||||
Promise.all(
|
||||
outcomeLinks.map(outcomeLink =>
|
||||
fetchUrl(
|
||||
fetchWithDispatch(
|
||||
`/api/v1/courses/${courseId}/outcome_results?user_ids[]=${studentId}&outcome_ids[]=${outcomeLink.outcome.id}&include[]=assignments&per_page=100`
|
||||
)
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue