Skip to content

Commit

Permalink
fix: check connection flow
Browse files Browse the repository at this point in the history
  • Loading branch information
phamhieu committed Apr 22, 2022
1 parent 5448556 commit 5524733
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 38 deletions.
3 changes: 2 additions & 1 deletion studio/components/layouts/ProjectLayout/BuildingState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ const ProjectBuildingState: FC<ProjectBuildingState> = ({ project }) => {
if (projectStatus && !projectStatus.error) {
const { status } = projectStatus
if (status === PROJECT_STATUS.ACTIVE_HEALTHY) {
clearInterval(checkServerInterval.current)

const res = await get(`${API_URL}/props/project/${project.ref}/connection-string`)
if (res && res.connectionString) {
app.onProjectConnectionStringUpdated(project.id, res.connectionString)
}
app.onProjectStatusUpdated(project.id, status)
clearInterval(checkServerInterval.current)
}
}
}
Expand Down
36 changes: 19 additions & 17 deletions studio/components/layouts/ProjectLayout/ConnectingState.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,45 @@
import Link from 'next/link'
import { FC, useEffect, useRef } from 'react'
import { observer } from 'mobx-react-lite'
import { Badge, IconLoader, IconMonitor, IconServer } from '@supabase/ui'

import { Project } from 'types'
import { API_URL } from 'lib/constants'
import { get, head } from 'lib/common/fetch'
import { headWithTimeout } from 'lib/common/fetch'
import { useStore } from 'hooks'
import ShimmerLine from 'components/ui/ShimmerLine'

interface Props {
project: Project
autoApiService: any
}

const ProjectRestartingState: FC<Props> = ({ project, autoApiService }) => {
const ProjectRestartingState: FC<Props> = ({ project }) => {
const { app } = useStore()
const checkProjectConnectionIntervalRef = useRef<number>()

useEffect(() => {
if (!autoApiService) return
if (!project.restUrl || !project.internalApiKey) return

// Check project connection status every 4 seconds
checkProjectConnectionIntervalRef.current = window.setInterval(testProjectConnection, 4000)
return () => {
clearInterval(checkProjectConnectionIntervalRef.current)
}
}, [autoApiService])
}, [project])

const testProjectConnection = async () => {
const API_KEY = autoApiService?.internalApiKey
const swaggerUrl = autoApiService?.restUrl

const headers: any = { apikey: API_KEY }
if (API_KEY?.length > 40) headers['Authorization'] = `Bearer ${API_KEY}`

const { error } = await head(swaggerUrl, [], { headers, credentials: 'omit' })
const headers = {
apikey: project.internalApiKey,
Authorization: `Bearer ${project.internalApiKey}`,
}
const { error } = await headWithTimeout(project.restUrl!, [], {
headers,
credentials: 'omit',
timeout: 2000,
})
console.error('testProjectConnection error: ', error)
if (error === undefined) {
clearInterval(checkProjectConnectionIntervalRef.current)
// We force a page refresh to retrigger TestConnection because
// there's no specific state for "RESTARTING" that TestConnection can listen to yet
window.location.replace(`/project/${project.ref}`)
app.onProjectPostgrestStatusUpdated(project.id, 'ONLINE')
}
}

Expand Down Expand Up @@ -78,4 +80,4 @@ const ProjectRestartingState: FC<Props> = ({ project, autoApiService }) => {
)
}

export default ProjectRestartingState
export default observer(ProjectRestartingState)
33 changes: 27 additions & 6 deletions studio/components/layouts/ProjectLayout/ProjectLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { FC, ReactNode } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useStore } from 'hooks'
import { PROJECT_STATUS } from 'lib/constants'

import Connecting from 'components/ui/Loading'
import NavigationBar from './NavigationBar/NavigationBar'
import ProductMenuBar from './ProductMenuBar'
import LayoutHeader from './LayoutHeader'
import TestConnection from './TestConnection'
import ConnectingState from './ConnectingState'
import BuildingState from './BuildingState'

interface Props {
title?: string
Expand Down Expand Up @@ -57,7 +59,7 @@ const ProjectLayout: FC<Props> = ({
)
}

export default observer(ProjectLayout)
export default ProjectLayout

interface MenuBarWrapperProps {
isLoading: boolean
Expand All @@ -73,19 +75,38 @@ interface ContentWrapperProps {
isLoading: boolean
}

/**
* Check project.status to show building state or error state
*
* TODO: how can we test project connection properly?
* ex: the status is ACTIVE_HEALTHY but the project instance is down.
*
* [Joshen] As of 210422: Current testing connection by pinging postgres
* Ideally we'd have a more specific monitoring of the project such as during restarts
* But that will come later: https://supabase.slack.com/archives/C01D6TWFFFW/p1650427619665549
*
* Just note that this logic does not differentiate between a "restarting" state and
* a "something is wrong and can't connect to project" state.
*
* [TODO] Next iteration should scrape long polling and just listen to the project's status
*/
const ContentWrapper: FC<ContentWrapperProps> = observer(({ isLoading, children }) => {
const { ui } = useStore()
const router = useRouter()
const requiresDbConnection: boolean = router.pathname !== '/project/[ref]/settings/general'
const isProjectBuilding = [PROJECT_STATUS.COMING_UP, PROJECT_STATUS.RESTORING].includes(
ui.selectedProject?.status ?? ''
)
const isProjectOffline = ui.selectedProject?.postgrestStatus === 'OFFLINE'

return (
<>
{isLoading || ui.selectedProject === undefined ? (
<Connecting />
) : requiresDbConnection ? (
<TestConnection project={ui.selectedProject!}>
<div className="flex flex-col flex-1 overflow-y-auto">{children}</div>
</TestConnection>
) : requiresDbConnection && isProjectOffline ? (
<ConnectingState project={ui.selectedProject} />
) : requiresDbConnection && isProjectBuilding ? (
<BuildingState project={ui.selectedProject} />
) : (
<>{children}</>
)}
Expand Down
2 changes: 1 addition & 1 deletion studio/components/layouts/ProjectLayout/TestConnection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const TestConnection: FC<Props> = ({ project, children }) => {
return (
<>
{!isProjectOnline ? (
<ConnectingState project={project} autoApiService={projectProps?.autoApiService} />
<ConnectingState project={project} />
) : isProjectActive ? (
children
) : isBuilding ? (
Expand Down
29 changes: 29 additions & 0 deletions studio/lib/common/fetch/head.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,32 @@ export async function head<T = any>(
return handleError(error, requestId)
}
}

export async function headWithTimeout<T = any>(
url: string,
headersToRetrieve: string[],
options?: { [prop: string]: any }
): Promise<SupaResponse<T>> {
const requestId = uuidv4()
try {
const timeout = options?.timeout ?? 60000
const controller = new AbortController()
const id = setTimeout(() => controller.abort(), timeout)
const { headers: optionHeaders, ...otherOptions } = options ?? {}
const headers = constructHeaders(requestId, optionHeaders)
const response = await fetch(url, {
method: 'HEAD',
credentials: 'include',
referrerPolicy: 'no-referrer-when-downgrade',
headers,
...otherOptions,
signal: controller.signal,
})
clearTimeout(id)

if (!response.ok) return handleResponseError(response, requestId)
return handleHeadResponse(response, requestId, headersToRetrieve)
} catch (error) {
return handleError(error, requestId)
}
}
18 changes: 12 additions & 6 deletions studio/pages/project/[ref]/settings/general.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC, useState } from 'react'
import { FC, useState } from 'react'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { toJS } from 'mobx'
Expand Down Expand Up @@ -30,8 +30,13 @@ const ProjectSettings = () => {

export default withAuth(observer(ProjectSettings))

const RestartServerButton: FC<any> = ({ projectRef }: any) => {
const { ui } = useStore()
interface RestartServerButtonProps {
projectId: number
projectRef: string
}
const RestartServerButton: FC<RestartServerButtonProps> = observer(({ projectRef, projectId }) => {
const { ui, app } = useStore()
const router = useRouter()
const [loading, setLoading] = useState(false)
const [isModalOpen, setIsModalOpen] = useState(false)

Expand All @@ -42,8 +47,9 @@ const RestartServerButton: FC<any> = ({ projectRef }: any) => {
setLoading(true)
try {
await post(`${API_URL}/projects/${projectRef}/restart`, {})
app.onProjectPostgrestStatusUpdated(projectId, 'OFFLINE')
ui.setNotification({ category: 'success', message: 'Restarting server' })
window.location.replace(`/project/${projectRef}`)
router.push(`/project/${projectRef}`)
} catch (error) {
ui.setNotification({ error, category: 'error', message: 'Unable to restart server' })
setLoading(false)
Expand All @@ -68,7 +74,7 @@ const RestartServerButton: FC<any> = ({ projectRef }: any) => {
</Button>
</>
)
}
})

const GeneralSettings = observer(() => {
const { app, ui } = useStore()
Expand Down Expand Up @@ -133,7 +139,7 @@ const GeneralSettings = observer(() => {
</p>
</div>
</div>
<RestartServerButton projectRef={project?.ref} />
{project && <RestartServerButton projectId={project.id} projectRef={project.ref} />}
</div>
</Panel.Content>
</Panel>
Expand Down
9 changes: 7 additions & 2 deletions studio/stores/app/AppStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface IAppStore {
onProjectDeleted: (project: any) => void
onProjectConnectionStringUpdated: (projectId: number, value: string) => void
onProjectStatusUpdated: (projectId: number, value: string) => void
onProjectPostgrestStatusUpdated: (projectId: number, value: 'OFFLINE' | 'ONLINE') => void
onOrgAdded: (org: any) => void
onOrgUpdated: (org: any) => void
onOrgDeleted: (org: any) => void
Expand Down Expand Up @@ -52,8 +53,6 @@ export default class AppStore implements IAppStore {
region: project.region,
inserted_at: project.inserted_at,
subscription_id: project.subscription_id,
kpsVersion: undefined,
connectionString: undefined,
}
this.projects.data[project.id] = temp
}
Expand Down Expand Up @@ -89,6 +88,12 @@ export default class AppStore implements IAppStore {
this.projects.data[projectId] = clone
}

onProjectPostgrestStatusUpdated(projectId: number, value: 'OFFLINE' | 'ONLINE') {
const clone = cloneDeep(this.projects.data[projectId])
clone.postgrestStatus = value
this.projects.data[projectId] = clone
}

onOrgUpdated(updatedOrg: Organization) {
if (updatedOrg && updatedOrg.id) {
const originalOrg = this.organizations.data[updatedOrg.id]
Expand Down
29 changes: 27 additions & 2 deletions studio/stores/app/ProjectStore.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Project, ResponseError } from 'types'
import { Project } from 'types'
import { IRootStore } from '../RootStore'
import { constructHeaders } from 'lib/api/apiHelpers'
import { get } from 'lib/common/fetch'
import { get, headWithTimeout } from 'lib/common/fetch'
import PostgresMetaInterface, { IPostgresMetaInterface } from '../common/PostgresMetaInterface'
import { PROJECT_STATUS } from 'lib/constants'

export interface IProjectStore extends IPostgresMetaInterface<Project> {
fetchDetail: (projectRef: string) => void
Expand All @@ -26,7 +27,31 @@ export default class ProjectStore extends PostgresMetaInterface<Project> {
const response = await get(url, { headers })
if (!response.error) {
const project = response as Project
if (
project.status === PROJECT_STATUS.ACTIVE_HEALTHY &&
project.restUrl &&
project.internalApiKey
) {
const success = await this.pingPostgrest(project.restUrl, project.internalApiKey)
project.postgrestStatus = success ? 'ONLINE' : 'OFFLINE'
}
this.data[project.id] = project
}
}

/**
* Send a HEAD request to postgrest OpenAPI
*
* @return true if there's no error else false
*/
async pingPostgrest(restUrl: string, apikey: string) {
const headers = { apikey, Authorization: `Bearer ${apikey}` }
const { error } = await headWithTimeout(restUrl, [], {
headers,
credentials: 'omit',
timeout: 2000,
})
console.log('pingPostgrest error: ', error)
return error === undefined
}
}
14 changes: 11 additions & 3 deletions studio/types/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,20 @@ export interface ProjectBase {

export interface Project extends ProjectBase {
// only available after projects.fetchDetail
kpsVersion?: string
connectionString?: string
kpsVersion?: string
internalApiKey?: string
restUrl?: string

/**
* postgrestStatus is available on client only.
* We use this status to check if a project instance is HEALTHY or not
* If not we will show ConnectingState and run a polling until it's back online
*/
postgrestStatus?: 'ONLINE' | 'OFFLINE'

// Possibly deprecated, just double check
// @deprecated, not available anymore
subscription_tier?: string
subscription_tier_prod_id?: string
}

export interface User {
Expand Down

0 comments on commit 5524733

Please sign in to comment.