Skip to content

Commit 227e84d

Browse files
authored
Merge pull request #264 from ember-fastboot/add-queue-management
Add sandbox queue management when using buildSandboxPerVisit
2 parents 0ca95fb + c3e4f94 commit 227e84d

File tree

7 files changed

+348
-19
lines changed

7 files changed

+348
-19
lines changed

README.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ let app = new FastBoot({
3636
// additional global properties to define within the sandbox
3737
});
3838
},
39+
40+
// optional number to be provided when using `buildSandboxPerVisit` which defines the queue size for sandboxes.
41+
// This number should represent your QPS of your service
42+
maxSandboxQueueSize: <Number> // defaults to 1 if not provided
3943
});
4044

4145
app.visit('/photos', options)
@@ -68,7 +72,7 @@ configuration:
6872
- `shouldRender`: boolean to indicate whether the app should do rendering or not. If set to false, it puts the app in routing-only. Defaults to true.
6973
- `disableShoebox`: boolean to indicate whether we should send the API data in the shoebox. If set to false, it will not send the API data used for rendering the app on server side in the index.html. Defaults to false.
7074
- `destroyAppInstanceInMs`: whether to destroy the instance in the given number of ms. This is a failure mechanism to not wedge the Node process
71-
- `buildSandboxPerVisit`: whether to create a new sandbox context per-visit (slows down each visit, but guarantees no prototype leakages can occur), or reuse the existing sandbox (faster per-request, but each request shares the same set of prototypes). Defaults to false.
75+
- `buildSandboxPerVisit`: whether to create a new sandbox context per-visit (slows down each visit, but guarantees no prototype leakages can occur), or reuse the existing sandbox (faster per-request, but each request shares the same set of prototypes). Defaults to false. When using this flag, also set `maxSandboxQueue` to represent the QPS of your application so that sandboxes can be queued for next requests. When not provided, it defaults to storing only one sandbox
7276

7377
### Build Your App
7478

@@ -107,6 +111,43 @@ rendering your Ember.js application using the [FastBoot App Server](https://gith
107111
Run `fastboot` with the `DEBUG` environment variable set to `fastboot:*`
108112
for detailed logging.
109113

114+
### Result
115+
116+
The result from `fastboot` is a `Result` object that has the following API:
117+
118+
```
119+
120+
type DOMContents = () => {
121+
/**
122+
The `<head>` contents generated by the visit.
123+
*/
124+
head: string;
125+
126+
/**
127+
The `<body>` contents generated by the visit.
128+
*/
129+
body: string;
130+
}
131+
132+
interface FastBootVisitResult {
133+
/**
134+
The serialized DOM contents after completing the `visit` request.
135+
136+
Note: this combines the `domContents.head` and `domContents.body`.
137+
*/
138+
html(): string;
139+
140+
domContents(): DOMContents
141+
142+
analytics: {
143+
/**
144+
* Boolean to know if the request used a prebuilt sandbox
145+
*/
146+
usedPrebuiltSandbox: <Boolean>
147+
}
148+
}
149+
```
150+
110151
### The Shoebox
111152

112153
You can pass application state from the FastBoot rendered application to

src/ember-app.js

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const FastBootInfo = require('./fastboot-info');
1414
const Result = require('./result');
1515
const FastBootSchemaVersions = require('./fastboot-schema-versions');
1616
const getPackageName = require('./utils/get-package-name');
17+
const Queue = require('./utils/queue');
1718

1819
/**
1920
* @private
@@ -29,6 +30,7 @@ class EmberApp {
2930
* @param {Object} options
3031
* @param {string} options.distPath - path to the built Ember application
3132
* @param {Function} [options.buildSandboxGlobals] - the function used to build the final set of global properties accesible within the sandbox
33+
* @param {Number} [options.maxSandboxQueueSize] - maximum sandbox queue size when using buildSandboxPerRequest flag.
3234
*/
3335
constructor(options) {
3436
this.buildSandboxGlobals = options.buildSandboxGlobals || defaultBuildSandboxGlobals;
@@ -64,9 +66,30 @@ class EmberApp {
6466
);
6567
this.scripts = buildScripts(filePaths);
6668

69+
// default to 1 if maxSandboxQueueSize is not defined so the sandbox is pre-warmed when process comes up
70+
const maxSandboxQueueSize = options.maxSandboxQueueSize || 1;
6771
// Ensure that the dist files can be evaluated and the `Ember.Application`
6872
// instance created.
69-
this.buildApp();
73+
this.buildSandboxQueue(maxSandboxQueueSize);
74+
}
75+
76+
/**
77+
* @private
78+
*
79+
* Function to build queue of sandboxes which is later leveraged if application is using `buildSandboxPerRequest`
80+
* flag. This is an optimization to help with performance.
81+
*
82+
* @param {Number} maxSandboxQueueSize - maximum size of queue (this is should be a derivative of your QPS)
83+
*/
84+
buildSandboxQueue(maxSandboxQueueSize) {
85+
this._sandboxApplicationInstanceQueue = new Queue(
86+
() => this.buildNewApplicationInstance(),
87+
maxSandboxQueueSize
88+
);
89+
90+
for (let i = 0; i < maxSandboxQueueSize; i++) {
91+
this._sandboxApplicationInstanceQueue.enqueue();
92+
}
7093
}
7194

7295
/**
@@ -237,27 +260,35 @@ class EmberApp {
237260
return app;
238261
}
239262

263+
/**
264+
* @private
265+
*
266+
* @param {Promise<instance>} appInstance - the instance that is pre-warmed or built on demand
267+
* @param {Boolean} isAppInstancePreBuilt - boolean representing how the instance was built
268+
*
269+
* @returns {Object}
270+
*/
271+
getAppInstanceInfo(appInstance, isAppInstancePreBuilt = true) {
272+
return { app: appInstance, isSandboxPreBuilt: isAppInstancePreBuilt };
273+
}
274+
240275
/**
241276
* @private
242277
*
243278
* Get the new sandbox off if it is being created, otherwise create a new one on demand.
244279
* The later is needed when the current request hasn't finished or wasn't build with sandbox
245280
* per request turned on and a new request comes in.
246281
*
282+
* @param {Boolean} buildSandboxPerVisit if true, a new sandbox will
283+
* **always** be created, otherwise one
284+
* is created for the first request
285+
* only
247286
*/
248-
async _getNewApplicationInstance() {
249-
let app;
250-
251-
if (this._pendingNewApplicationInstance) {
252-
let pendingAppInstancePromise = this._pendingNewApplicationInstance;
253-
this._pendingNewApplicationInstance = undefined;
254-
app = await pendingAppInstancePromise;
255-
} else {
256-
// if there is no current pending application instance, create a new one on-demand.
257-
app = await this.buildApp();
258-
}
287+
async getNewApplicationInstance() {
288+
const queueObject = this._sandboxApplicationInstanceQueue.dequeue();
289+
const app = await queueObject.item;
259290

260-
return app;
291+
return this.getAppInstanceInfo(app, queueObject.isItemPreBuilt);
261292
}
262293

263294
/**
@@ -285,13 +316,19 @@ class EmberApp {
285316
async _visit(path, fastbootInfo, bootOptions, result, buildSandboxPerVisit) {
286317
let shouldBuildApp = buildSandboxPerVisit || this._applicationInstance === undefined;
287318

288-
let app = shouldBuildApp ? await this._getNewApplicationInstance() : this._applicationInstance;
319+
let { app, isSandboxPreBuilt } = shouldBuildApp
320+
? await this.getNewApplicationInstance()
321+
: this.getAppInstanceInfo(this._applicationInstance);
289322

290323
if (buildSandboxPerVisit) {
291324
// entangle the specific application instance to the result, so it can be
292325
// destroyed when result._destroy() is called (after the visit is
293326
// completed)
294327
result.applicationInstance = app;
328+
329+
// we add analytics information about the current request to know
330+
// whether it used sandbox from the pre-built queue or built on demand.
331+
result.analytics.usedPrebuiltSandbox = isSandboxPreBuilt;
295332
} else {
296333
// save the created application instance so that we can clean it up when
297334
// this instance of `src/ember-app.js` is destroyed (e.g. reload)
@@ -387,7 +424,7 @@ class EmberApp {
387424
if (buildSandboxPerVisit) {
388425
// if sandbox was built for this visit, then build a new sandbox for the next incoming request
389426
// which is invoked using buildSandboxPerVisit
390-
this._pendingNewApplicationInstance = this.buildNewApplicationInstance();
427+
this._sandboxApplicationInstanceQueue.enqueue();
391428
}
392429
}
393430

src/index.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ class FastBoot {
4141
* @param {string} options.distPath the path to the built Ember application
4242
* @param {Boolean} [options.resilient=false] if true, errors during rendering won't reject the `visit()` promise but instead resolve to a {@link Result}
4343
* @param {Function} [options.buildSandboxGlobals] a function used to build the final set of global properties setup within the sandbox
44+
* @param {Number} [options.maxSandboxQueueSize] - maximum sandbox queue size when using buildSandboxPerRequest flag.
4445
*/
4546
constructor(options = {}) {
46-
let { distPath, buildSandboxGlobals } = options;
47+
let { distPath, buildSandboxGlobals, maxSandboxQueueSize } = options;
4748

4849
this.resilient = 'resilient' in options ? Boolean(options.resilient) : false;
4950

@@ -58,8 +59,9 @@ class FastBoot {
5859
}
5960

6061
this.buildSandboxGlobals = buildSandboxGlobals;
62+
this.maxSandboxQueueSize = maxSandboxQueueSize;
6163

62-
this._buildEmberApp(this.distPath, this.buildSandboxGlobals);
64+
this._buildEmberApp(this.distPath, this.buildSandboxGlobals, maxSandboxQueueSize);
6365
}
6466

6567
/**
@@ -106,7 +108,11 @@ class FastBoot {
106108
this._buildEmberApp(distPath);
107109
}
108110

109-
_buildEmberApp(distPath = this.distPath, buildSandboxGlobals = this.buildSandboxGlobals) {
111+
_buildEmberApp(
112+
distPath = this.distPath,
113+
buildSandboxGlobals = this.buildSandboxGlobals,
114+
maxSandboxQueueSize = this.maxSandboxQueueSize
115+
) {
110116
if (!distPath) {
111117
throw new Error(
112118
'You must instantiate FastBoot with a distPath ' +
@@ -124,6 +130,7 @@ class FastBoot {
124130
this._app = new EmberApp({
125131
distPath,
126132
buildSandboxGlobals,
133+
maxSandboxQueueSize,
127134
});
128135
}
129136
}

src/result.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Result {
2020
this._fastbootInfo = fastbootInfo;
2121
this.applicationInstance = undefined;
2222
this.applicationInstanceInstance = undefined;
23+
this.analytics = {};
2324
}
2425

2526
/**

src/utils/queue.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict';
2+
3+
const debug = require('debug')('fastboot:ember-app');
4+
5+
/**
6+
* Utility Queue class to store queue of sandboxes that can be leveraged when using `buildSandboxPerVisit`.
7+
*
8+
* @public
9+
*/
10+
class Queue {
11+
constructor(builderFn, maxSize = 1) {
12+
this.items = [];
13+
this.maxSize = maxSize;
14+
this.builderFn = builderFn;
15+
}
16+
17+
_buildItem() {
18+
return this.builderFn();
19+
}
20+
21+
_addToQueue() {
22+
this.items.push(this._buildItem());
23+
}
24+
25+
enqueue() {
26+
// when the queue is not full, we add the item into the queue, otherwise ignore adding
27+
// to the queue.
28+
if (!this.isFull()) {
29+
this._addToQueue();
30+
} else {
31+
debug('Ignoring adding appInstance to queue as Queue is already full!');
32+
}
33+
}
34+
35+
dequeue() {
36+
if (this.isEmpty()) {
37+
// build on demand if the queue does not have a pre-warmed version to avoid starving
38+
// the system
39+
return { item: this._buildItem(), isItemPreBuilt: false };
40+
} else {
41+
// return a pre-warmed version
42+
return { item: this.items.shift(), isItemPreBuilt: true };
43+
}
44+
}
45+
46+
isEmpty() {
47+
return this.size() === 0;
48+
}
49+
50+
size() {
51+
return this.items.length;
52+
}
53+
54+
isFull() {
55+
return this.size() === this.maxSize;
56+
}
57+
}
58+
59+
module.exports = Queue;

0 commit comments

Comments
 (0)