Skip to content

Set max session idle time #642

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

Merged
merged 15 commits into from
Jun 14, 2022
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 4.6.0
- Enforce a maximum value of 5000 ms for the `http2SessionIdleTime` option [#642](https://github.com/fauna/faunadb-js/pull/642)
- Add checks to `http2SessionIdleTime` so that sane defaults are used in case an invalid value is configured
- Add the missing Native.ROLES type [#638](https://github.com/fauna/faunadb-js/pull/638)

## 4.5.4
- Disable ability to configure a client and the query method from returning metrics when calling query - fixing bug introduced in 4.5.3 that breaks backward compatibility. Continue supporting queryWithMetrics. [#633](https://github.com/fauna/faunadb-js/pull/633).

Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ time, and transaction retires consumed by your query:
} // usage data
}
```

Metrics returned in the response will be of `number` data type.

#### Pagination Helpers

This driver contains helpers to provide a simpler API for consuming paged
Expand Down Expand Up @@ -281,13 +283,13 @@ have been resolved, the client will keep the session open for a period of time

The `http2SessionIdleTime` parameter may be used to control how long the HTTP/2
session remains open while the connection is idle. To save on the overhead of
closing and re-opening the session, set `http2SessionIdleTime` to a longer time
--- or even `Infinity`, to keep the session alive indefinitely.
closing and re-opening the session, set `http2SessionIdleTime` to a longer time.
The default value is 500ms and the maximum value is 5000ms.

While an HTTP/2 session is alive, the client will hold the Node.js event loop
open; this prevents the process from terminating. Call `Client#close` to manually
close the session and allow the process to terminate. This is particularly
important if `http2SessionIdleTime` is long or `Infinity`:
important if `http2SessionIdleTime` is long:

```javascript
// sample.js (run it with "node sample.js" command)
Expand All @@ -296,8 +298,8 @@ const { Client, query: Q } = require('faunadb')
async function main() {
const client = new Client({
secret: 'YOUR_FAUNADB_SECRET',
http2SessionIdleTime: Infinity,
// ^^^ Infinity or non-negative integer
http2SessionIdleTime: 1000,
// ^^^ Non-negative integer
})
const output = await client.query(Q.Add(1, 1))

Expand All @@ -310,7 +312,6 @@ async function main() {
main().catch(console.error)
```


## Known issues

### Using with Cloudflare Workers
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "faunadb",
"version": "4.5.4",
"version": "4.6.0",
"apiVersion": "4",
"description": "FaunaDB Javascript driver for Node.JS and Browsers",
"homepage": "https://fauna.com",
Expand Down
49 changes: 31 additions & 18 deletions src/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,10 @@ var values = require('./values')
* Sets the maximum amount of time (in milliseconds) for query execution on the server
* @param {?number} options.http2SessionIdleTime
* Sets the maximum amount of time (in milliseconds) an HTTP2 session may live
* when there's no activity. Must either be a non-negative integer, or Infinity to allow the
* HTTP2 session to live indefinitely (use `Client#close` to manually terminate the client).
* Only applicable for NodeJS environment (when http2 module is used). Default is 500ms;
* when there's no activity. Must be a non-negative integer, with a maximum value of 5000.
* If an invalid value is passed a default of 500 ms will be applied. If a value
* exceeding 5000 ms is passed (e.g. Infinity) the maximum of 5000 ms will be applied.
* Only applicable for NodeJS environment (when http2 module is used).
* can also be configured via the FAUNADB_HTTP2_SESSION_IDLE_TIME environment variable
* which has the highest priority and overrides the option passed into the Client constructor.
* @param {?boolean} options.checkNewVersion
Expand All @@ -169,7 +170,11 @@ var values = require('./values')
* Disabled by default. Controls whether or not query metrics are returned.
*/
function Client(options) {
var http2SessionIdleTime = getHttp2SessionIdleTime()
const http2SessionIdleTime = getHttp2SessionIdleTime(
options ? options.http2SessionIdleTime : undefined
)

if (options) options.http2SessionIdleTime = http2SessionIdleTime

options = util.applyDefaults(options, {
domain: 'db.fauna.com',
Expand All @@ -182,14 +187,10 @@ function Client(options) {
headers: {},
fetch: undefined,
queryTimeout: null,
http2SessionIdleTime: http2SessionIdleTime.value,
http2SessionIdleTime,
checkNewVersion: false,
})

if (http2SessionIdleTime.shouldOverride) {
options.http2SessionIdleTime = http2SessionIdleTime.value
}

this._observer = options.observer
this._http = new http.HttpClient(options)
this.stream = stream.StreamAPI(this)
Expand Down Expand Up @@ -384,17 +385,29 @@ Client.prototype._handleRequestResult = function(response, result, options) {
errors.FaunaHTTPError.raiseForStatusCode(result)
}

function getHttp2SessionIdleTime() {
var fromEnv = util.getEnvVariable('FAUNADB_HTTP2_SESSION_IDLE_TIME')
var parsed =
// Allow either "Infinity" or parsable integer string.
fromEnv === 'Infinity' ? Infinity : parseInt(fromEnv, 10)
var useEnvVar = !isNaN(parsed)
function getHttp2SessionIdleTime(configuredIdleTime) {
const maxIdleTime = 5000
const defaultIdleTime = 500
const envIdleTime = util.getEnvVariable('FAUNADB_HTTP2_SESSION_IDLE_TIME')

return {
shouldOverride: useEnvVar,
value: useEnvVar ? parsed : 500,
var value = defaultIdleTime
// attemp to set the idle time to the env value and then the configured value
const values = [envIdleTime, configuredIdleTime]
for (const rawValue of values) {
const parsedValue = parseInt(rawValue, 10)
const isNegative = parsedValue < 0
const isInfinity = rawValue === 'Infinity'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be easier to do this check first?

The overall check could be simpler, too:

// Handle Infinity, and NaN (for any other string)
const parsedValue = rawValue === 'Infinity'
  ? Number.MAX_SAFE_INTEGER
  : parseInt(rawValue, 10) || defaultIdleTime

// Handle upper bound
if (parsedValue > maxIdleTime) parsedValue = maxIdleTime

// Handle lower bound
if (parsedValue < 0) parsedValue = defaultIdleTime

return parsedValue

const isGreaterThanMax = parsedValue > maxIdleTime || isInfinity
// if we didn't get infinity or a positive integer move to the next value
if (isNegative) continue
if (!isInfinity && !parsedValue) continue
// if we did get something valid constrain it to the ceiling
value = parsedValue
if (isGreaterThanMax) value = maxIdleTime
break
}

return value
}

module.exports = Client
120 changes: 117 additions & 3 deletions test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ var json = require('../src/_json')
var client

describe('Client', () => {
const env = process.env

beforeAll(() => {
// Hideous way to ensure that the client is initialized.
client = util.client()

return client.query(query.CreateCollection({ name: 'my_collection' }))
})

beforeEach(() => {
process.env = { ...env }
})

afterEach(() => {
process.env = env
util.clearBrowserSimulation()
})

Expand Down Expand Up @@ -65,7 +71,6 @@ describe('Client', () => {
toEqual(['metrics', 'value'])
})


test('paginates', () => {
return createDocument().then(function(document) {
return client.paginate(document.ref).each(function(page) {
Expand Down Expand Up @@ -176,7 +181,7 @@ describe('Client', () => {

test('Client#close call on Http2Adapter-based Client', async () => {
const client = util.getClient({
http2SessionIdleTime: Infinity,
http2SessionIdleTime: 5000,
})

await client.ping()
Expand Down Expand Up @@ -412,6 +417,115 @@ describe('Client', () => {
requiredKeys.every(key => driverEnvHeader.includes(key))
).toBeDefined()
})

test('http2SessionIdleTime env overrides client config', async () => {
var client
var internalIdleTime
const maxIdleTime = 5000
const defaultIdleTime = 500

process.env.FAUNADB_HTTP2_SESSION_IDLE_TIME = '999'
client = util.getClient({
http2SessionIdleTime: 2500,
})
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(999)

process.env.FAUNADB_HTTP2_SESSION_IDLE_TIME = maxIdleTime + 1
client = util.getClient({
http2SessionIdleTime: 2500,
})
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(maxIdleTime)

process.env.FAUNADB_HTTP2_SESSION_IDLE_TIME = 'Infinity'
client = util.getClient({
http2SessionIdleTime: 2500,
})
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(maxIdleTime)

process.env.FAUNADB_HTTP2_SESSION_IDLE_TIME = 'Cat'
client = util.getClient({
http2SessionIdleTime: 2500,
})
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(2500)

process.env.FAUNADB_HTTP2_SESSION_IDLE_TIME = 'Cat'
client = util.getClient({
http2SessionIdleTime: "Cat",
})
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(defaultIdleTime)

process.env.FAUNADB_HTTP2_SESSION_IDLE_TIME = '-999'
client = util.getClient({
http2SessionIdleTime: 2500,
})
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(2500)

process.env.FAUNADB_HTTP2_SESSION_IDLE_TIME = '-999'
client = util.getClient({
http2SessionIdleTime: -999,
})
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(defaultIdleTime)

process.env.FAUNADB_HTTP2_SESSION_IDLE_TIME = '-999'
client = util.getClient({
http2SessionIdleTime: "Infinity",
})
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(maxIdleTime)
})

test('http2SessionIdleTime respects the max and default', async () => {
var client
var internalIdleTime
const maxIdleTime = 5000
const defaultIdleTime = 500
process.env.FAUNADB_HTTP2_SESSION_IDLE_TIME = undefined

client = util.getClient({
http2SessionIdleTime: 'Infinity',
})
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(maxIdleTime)

client = util.getClient({
http2SessionIdleTime: maxIdleTime + 1,
})
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(maxIdleTime)

client = util.getClient({})
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(defaultIdleTime)

client = util.getClient({ http2SessionIdleTime: null })
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(defaultIdleTime)

client = util.getClient({
http2SessionIdleTime: 'Cat',
})
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(defaultIdleTime)

client = util.getClient({
http2SessionIdleTime: 2500,
})
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(2500)

client = util.getClient({
http2SessionIdleTime: -2500,
})
internalIdleTime = client._http._adapter._http2SessionIdleTime
expect(internalIdleTime).toBe(defaultIdleTime)
})
})

function assertObserverStats(metrics, name) {
Expand Down
42 changes: 42 additions & 0 deletions test/stream.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,48 @@ describe('StreamAPI', () => {
})
.start()
})

test('stays open beyond the value set for http2SessionIdleTime', async () => {
let seen_event = false
const idleTime = 1000
const client = util.getClient({
secret: key.secret,
http2SessionIdleTime: idleTime,
})
const oldTimestamp = doc.ts

const assertActiveSessions = length =>
expect(Object.keys(client._http._adapter._sessionMap).length).toBe(
length
)

stream = client.stream
.document(doc.ref)
.on('version', (event) => {
seen_event = true
expect(event.action).toEqual("update")
expect(event.document.ts).toBeGreaterThan(oldTimestamp)
})

stream.start()

await util.delay(idleTime + 500)
assertActiveSessions(1)

const { ts: newTimestamp } = await client.query(q.Update(doc.ref, {}))
expect(newTimestamp).toBeGreaterThan(oldTimestamp)
await util.delay(idleTime + 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be worth checking that the stream is still open beyond the maximum value of http2SessionIdleTime? As written, this test doesn't really demonstrate that a stream is held open indefinitely, just that it is held open for 2002 ms (which is well within 5000ms).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test should be updated now

assertActiveSessions(1)
expect(seen_event).toBe(true)
seen_event = false
await util.delay(idleTime + 1)
expect(seen_event).toBe(false)
assertActiveSessions(1)

stream.close()
await util.delay(idleTime + 1)
assertActiveSessions(0)
})
})

describe('document', () => {
Expand Down