Skip to content

Commit

Permalink
Make schema reloading more resilient. Closes graphql#940 (graphql#951)
Browse files Browse the repository at this point in the history
* Make schema reloading more resilient. Closes graphql#940

* Regarding "If there’s an active introspection query, wait for it’s result before sending another one" is default behavior when schema is reloading. The changes I made to account for this are unnecessary and actually cause polling to stop if the endpoint is unreachable.

* getIsPollingSchema catching regex error

* add schema reference equality & printing cache

* improve code readability

* cleanup: remove dead code

* fix build: use lru-cache. less unneeded repaints

* fix lru cache usage
  • Loading branch information
timsuchanek authored and huv1k committed Feb 1, 2019
1 parent 42ffbec commit 607d8d5
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 76 deletions.
1 change: 1 addition & 0 deletions packages/graphql-playground-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"why-did-you-update": "0.1.1"
},
"dependencies": {
"@types/lru-cache": "^4.1.1",
"apollo-link": "^1.0.7",
"apollo-link-http": "^1.3.2",
"apollo-link-ws": "1.0.8",
Expand Down
32 changes: 22 additions & 10 deletions packages/graphql-playground-react/src/components/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
} from '../state/sessions/actions'
import { setConfigString } from '../state/general/actions'
import { initState } from '../state/workspace/actions'
import { GraphQLSchema, printSchema } from 'graphql'
import { GraphQLSchema } from 'graphql'
import { createStructuredSelector } from 'reselect'
import {
getIsConfigTab,
Expand All @@ -50,6 +50,7 @@ import { getWorkspaceId } from './Playground/util/getWorkspaceId'
import { getSettings, getSettingsString } from '../state/workspace/reducers'
import { Backoff } from './Playground/util/fibonacci-backoff'
import { debounce } from 'lodash'
import { cachedPrintSchema } from './util'

export interface Response {
resultID: string
Expand Down Expand Up @@ -173,6 +174,7 @@ export class Playground extends React.PureComponent<Props & ReduxProps, State> {
private backoff: Backoff
private initialIndex: number = -1
private mounted = false
private initialSchemaFetch = true

constructor(props: Props & ReduxProps) {
super(props)
Expand Down Expand Up @@ -270,11 +272,14 @@ export class Playground extends React.PureComponent<Props & ReduxProps, State> {
})
if (schema) {
this.updateSchema(currentSchema, schema.schema, props)
this.props.schemaFetchingSuccess(
data.endpoint,
schema.tracingSupported,
props.isPollingSchema,
)
if (this.initialSchemaFetch) {
this.props.schemaFetchingSuccess(
data.endpoint,
schema.tracingSupported,
props.isPollingSchema,
)
this.initialSchemaFetch = false
}
this.backoff.stop()
}
} catch (e) {
Expand Down Expand Up @@ -367,10 +372,17 @@ export class Playground extends React.PureComponent<Props & ReduxProps, State> {
props: Readonly<{ children?: React.ReactNode }> &
Readonly<Props & ReduxProps>,
) {
const currentSchemaStr = currentSchema ? printSchema(currentSchema) : null
const newSchemaStr = printSchema(newSchema)
if (newSchemaStr !== currentSchemaStr || !props.isPollingSchema) {
this.setState({ schema: newSchema })
// first check for reference equality
if (currentSchema !== newSchema) {
// if references are not equal, do an equality check on the printed schema
const currentSchemaStr = currentSchema
? cachedPrintSchema(currentSchema)
: null
const newSchemaStr = cachedPrintSchema(newSchema)

if (newSchemaStr !== currentSchemaStr || !props.isPollingSchema) {
this.setState({ schema: newSchema })
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Map, set } from 'immutable'
import { makeOperation } from './util/makeOperation'
import { parseHeaders } from './util/parseHeaders'
import { LinkCreatorProps } from '../../state/sessions/fetchingSagas'
import * as LRU from 'lru-cache'

export interface TracingSchemaTuple {
schema: GraphQLSchema
Expand All @@ -18,19 +19,53 @@ export interface SchemaFetchProps {

export type LinkGetter = (session: LinkCreatorProps) => { link: ApolloLink }

/**
* The SchemaFetcher class servers the purpose of providing the GraphQLSchema.
* All sagas and every part of the UI is using this as a singleton to prevent
* unnecessary calls to the server. We're not storing this information in Redux,
* as it's a good practice to only store serializable data in Redux.
* GraphQLSchema objects are serializable, but can easily exceed the localStorage
* max. Another reason to keep this in a separate class is, that we have more
* advanced requirements like caching.
*/
export class SchemaFetcher {
cache: Map<string, TracingSchemaTuple>
/**
* The `sessionCache` property is used for UI components, that need fast access to the current schema.
* If the relevant information of the session didn't change (endpoint and headers),
* the cached schema will be returned.
*/
sessionCache: LRU.Cache<string, TracingSchemaTuple>
/**
* The `schemaInstanceCache` property is used to prevent unnecessary buildClientSchema calls.
* It's tested by stringifying the introspection result, which is orders of magnitude
* faster than rebuilding the schema.
*/
schemaInstanceCache: LRU.Cache<string, GraphQLSchema>
/**
* The `linkGetter` property is a callback that provides an ApolloLink instance.
* This can be overriden by the user.
*/
linkGetter: LinkGetter
/**
* In order to prevent duplicate fetching of the same schema, we keep track
* of all subsequent calls to `.fetch` with the `fetching` property.
*/
fetching: Map<string, Promise<any>>
/**
* Other parts of the application can subscribe to change of a schema for a
* particular session. These subscribers are being kept track of in the
* `subscriptions` property
*/
subscriptions: Map<string, (schema: GraphQLSchema) => void> = Map()
constructor(linkGetter: LinkGetter) {
this.cache = Map()
this.sessionCache = new LRU<string, TracingSchemaTuple>({ max: 10 })
this.schemaInstanceCache = new LRU({ max: 10 })
this.fetching = Map()
this.linkGetter = linkGetter
}
async fetch(session: SchemaFetchProps) {
const hash = this.hash(session)
const cachedSchema = this.cache.get(hash)
const cachedSchema = this.sessionCache.get(hash)
if (cachedSchema) {
return cachedSchema
}
Expand All @@ -52,6 +87,19 @@ export class SchemaFetcher {
hash(session: SchemaFetchProps) {
return `${session.endpoint}~${session.headers || ''}`
}
private getSchema(data: any) {
const schemaString = JSON.stringify(data)
const cachedSchema = this.schemaInstanceCache.get(schemaString)
if (cachedSchema) {
return cachedSchema
}

const schema = buildClientSchema(data as any)

this.schemaInstanceCache.set(schemaString, schema)

return schema
}
private fetchSchema(
session: SchemaFetchProps,
): Promise<{ schema: GraphQLSchema; tracingSupported: boolean } | null> {
Expand Down Expand Up @@ -83,15 +131,15 @@ export class SchemaFetcher {
throw new NoSchemaError(endpoint)
}

const schema = buildClientSchema(schemaData.data as any)
const schema = this.getSchema(schemaData.data as any)
const tracingSupported =
(schemaData.extensions && Boolean(schemaData.extensions.tracing)) ||
false
const result = {
const result: TracingSchemaTuple = {
schema,
tracingSupported,
}
this.cache = this.cache.set(this.hash(session), result)
this.sessionCache.set(this.hash(session), result)
resolve(result)
this.fetching = this.fetching.remove(hash)
const subscription = this.subscriptions.get(hash)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import PollingIcon from './PollingIcon'

export interface Props {
interval: number
isReloadingSchema: boolean
onReloadSchema: () => void
}

Expand Down Expand Up @@ -47,17 +46,15 @@ class SchemaPolling extends React.Component<Props, State> {
}
}
componentWillReceiveProps(nextProps: Props) {
if (nextProps.isReloadingSchema !== this.props.isReloadingSchema) {
this.updatePolling(nextProps)
}
this.updatePolling(nextProps)
}

render() {
return <PollingIcon animate={this.state.windowVisible} />
}
private updatePolling = (props: Props = this.props) => {
this.clearTimer()
if (!props.isReloadingSchema && this.state.windowVisible) {
if (this.state.windowVisible) {
// timer starts only when introspection not in flight
this.timer = setInterval(() => props.onReloadSchema(), props.interval)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import * as React from 'react'
import ReloadIcon from './Reload'
import Polling from './Polling'
import { ISettings } from '../../../types'
import { createStructuredSelector } from 'reselect'
import { getIsReloadingSchema } from '../../../state/sessions/selectors'
import { connect } from 'react-redux'

export interface Props {
isPollingSchema: boolean
Expand All @@ -10,12 +13,11 @@ export interface Props {
settings: ISettings
}

export default (props: Props) => {
const SchemaReload = (props: Props) => {
if (props.isPollingSchema) {
return (
<Polling
interval={props.settings['schema.polling.interval']}
isReloadingSchema={props.isReloadingSchema}
onReloadSchema={props.onReloadSchema}
/>
)
Expand All @@ -27,3 +29,9 @@ export default (props: Props) => {
/>
)
}

const mapStateToProps = createStructuredSelector({
isReloadingSchema: getIsReloadingSchema,
})

export default connect(mapStateToProps)(SchemaReload)
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { createStructuredSelector } from 'reselect'
import {
getEndpoint,
getSelectedSession,
getIsReloadingSchema,
getEndpointUnreachable,
getIsPollingSchema,
} from '../../../state/sessions/selectors'
Expand All @@ -29,7 +28,6 @@ export interface Props {
endpoint: string
shareEnabled?: boolean
fixedEndpoint?: boolean
isReloadingSchema: boolean
isPollingSchema: boolean
endpointUnreachable: boolean

Expand Down Expand Up @@ -83,7 +81,6 @@ class TopBar extends React.Component<Props, {}> {
<SchemaReload
settings={settings}
isPollingSchema={this.props.isPollingSchema}
isReloadingSchema={this.props.isReloadingSchema}
onReloadSchema={this.props.refetchSchema}
/>
</div>
Expand Down Expand Up @@ -157,7 +154,6 @@ class TopBar extends React.Component<Props, {}> {
const mapStateToProps = createStructuredSelector({
endpoint: getEndpoint,
fixedEndpoint: getFixedEndpoint,
isReloadingSchema: getIsReloadingSchema,
isPollingSchema: getIsPollingSchema,
endpointUnreachable: getEndpointUnreachable,
settings: getSettings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,10 +427,8 @@ class PlaygroundWrapper extends React.Component<

handleSaveConfig = () => {
/* tslint:disable-next-line */
console.log('handleSaveConfig called')
if (typeof this.props.onSaveConfig === 'function') {
/* tslint:disable-next-line */
console.log('calling this.props.onSaveConfig', this.state.configString)
this.props.onSaveConfig(this.state.configString!)
}
}
Expand Down
19 changes: 19 additions & 0 deletions packages/graphql-playground-react/src/components/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { GraphQLConfig, GraphQLConfigEnpointConfig } from '../graphqlConfig'
import { GraphQLSchema, printSchema } from 'graphql'
import * as LRU from 'lru-cache'

export function getActiveEndpoints(
config: GraphQLConfig,
Expand Down Expand Up @@ -32,3 +34,20 @@ export function getEndpointFromEndpointConfig(
}
}
}

const printSchemaCache: LRU.Cache<GraphQLSchema, string> = new LRU({ max: 10 })
/**
* A cached version of `printSchema`
* @param schema GraphQLSchema instance
*/
export function cachedPrintSchema(schema: GraphQLSchema) {
const cachedString = printSchemaCache.get(schema)
if (cachedString) {
return cachedString
}

const schemaString = printSchema(schema)
printSchemaCache.set(schema, schemaString)

return schemaString
}
Loading

0 comments on commit 607d8d5

Please sign in to comment.