node.js patterns for discerning developers

76
Node.js Patterns For the Discerning Developer ron Cois, Ph.D. :: Carnegie Mellon University, SEI

Upload: cacois

Post on 06-May-2015

16.779 views

Category:

Technology


1 download

DESCRIPTION

Slides from my talk "Node.js Patterns for Discerning Developers" given at Pittsburgh TechFest 2013. This talk detailed common design pattern for Node.js, as well as common anti-patterns to avoid.

TRANSCRIPT

Page 1: Node.js Patterns for Discerning Developers

Node.js PatternsFor the Discerning Developer

C. Aaron Cois, Ph.D. :: Carnegie Mellon University, SEI

Page 2: Node.js Patterns for Discerning Developers

Me

@aaroncois

www.codehenge.net

github.com/cacois

Disclaimer: Though I am an employee of the Software Engineering Institute at Carnegie Mellon University, this work was not funded by the SEI and does not reflect the work or opinions of the SEI or its customers.

Page 3: Node.js Patterns for Discerning Developers

Let’s talk about

Page 4: Node.js Patterns for Discerning Developers

Node.js Basics

• JavaScript

• Asynchronous

• Non-blocking I/O

• Event-driven

Page 5: Node.js Patterns for Discerning Developers

So, JavaScript?

Page 6: Node.js Patterns for Discerning Developers

The Basics

Page 7: Node.js Patterns for Discerning Developers

Prototype-based Programming

• JavaScript has no classes• Instead, functions define objects

function Person() {}

var p = new Person();

Image: http://tech2.in.com/features/gaming/five-wacky-gaming-hardware-to-look-forward-to/315742

Prototype

Page 8: Node.js Patterns for Discerning Developers

Classless Programming

What do classes do for us?

• Define local scope / namespace• Allow private attributes / methods• Encapsulate code• Organize applications in an object-

oriented way

Page 9: Node.js Patterns for Discerning Developers

Prototype-based Programming

function Person(firstname, lastname){   this.firstname = firstname;   this.lastname = lastname; }  var p = new Person(“Philip”, “Fry”);

What else can do that?

Page 10: Node.js Patterns for Discerning Developers

Prototype Inheritance

function Person(firstname, lastname){   this.firstname = firstname;   this.lastname = lastname; } // Create new class Employee = Person;//Inherit from superclass

Employee.prototype = {    marital_status: 'single',      salute: function() {     return 'My name is ' + this.firstname;    } }

var p = new Employee (“Philip”, “Fry”);

Page 11: Node.js Patterns for Discerning Developers

Watch out! function Person(firstname, lastname){   this.firstname = firstname;   this.lastname = lastname; } // Create new class Employee = Person;//Inherit from superclass

Employee.prototype = {    marital_status: 'single',      salute: function() {     return 'My name is ' + this.firstname;    } }

var p = new Employee (“Philip”, “Fry”);

The ‘new’ is very important!

If you forget, your new object will have global scope internally

Page 12: Node.js Patterns for Discerning Developers

Another option function Person(firstname, lastname){   this.firstname = firstname;   this.lastname = lastname; } Employee = Person;//Inherit from superclass

Employee.prototype = {    marital_status: 'single',      salute: function() {     return 'My name is ' + this.firstname;    } }

var p = Object.create(Employee); p.firstname = 'Philip'; p.lastname = 'Fry';

Works, but you can’t initialize attributes in constructor

Page 13: Node.js Patterns for Discerning Developers

Anti-Pattern: JavaScript Imports

• Spread code around files• Link libraries

• No way to maintain private local scope/state/namespace

• Leads to:– Name collisions– Unnecessary access

Page 14: Node.js Patterns for Discerning Developers

Pattern: Modules

• An elegant way of encapsulating and reusing code

• Adapted from YUI, a few years before Node.js

• Takes advantage of the anonymous closure features of JavaScript

Image: http://wallpapersus.com/

Page 15: Node.js Patterns for Discerning Developers

Modules in the Wild

var http = require('http'),     io = require('socket.io'),     _ = require('underscore');

If you’ve programmed in Node, this looks familiar

Page 16: Node.js Patterns for Discerning Developers

Anatomy of a module

var privateVal = 'I am Private!';   module.exports = {    answer: 42,      add: function(x, y) {          return x + y;    }  }

mymodule.js

Page 17: Node.js Patterns for Discerning Developers

Usage

mod = require('./mymodule');  console.log('The answer: '+ mod.answer);  var sum = mod.add(4,5); console.log('Sum: ' + sum);

Page 18: Node.js Patterns for Discerning Developers

Modules are used everywhere // User model   var mongoose = require('mongoose')         , Schema = mongoose.Schema;    var userSchema = new Schema({      name: {type: String, required: true},      email: {type: String, required: true},      githubid: String,      twitterid: String,      dateCreated: {type: Date, default: Date.now}   });    userSchema.methods.validPassword = function validPass(pass) {      // validate password…   }    module.exports = mongoose.model('User', userSchema);

Page 19: Node.js Patterns for Discerning Developers

My config files? Modules.

   var config = require('config.js');  console.log('Configured user is: ' + config.user);

   module.exports = { user: 'maurice.moss' }

config.js

app.js

Page 20: Node.js Patterns for Discerning Developers

Asynchronous

Page 21: Node.js Patterns for Discerning Developers

Asynchronous Programming

• Node is entirely asynchronous• You have to think a bit differently• Failure to understand the event loop

and I/O model can lead to anti-patterns

Page 22: Node.js Patterns for Discerning Developers

Event Loop

Node.js Event Loop

Node app

Page 23: Node.js Patterns for Discerning Developers

Event Loop

Node.js Event Loop

Node apps pass async tasks to the event loop, along with a callback

(function, callback)

Node app

Page 24: Node.js Patterns for Discerning Developers

Event Loop

Node.js Event Loop

The event loop efficiently manages a thread pool and executes tasks efficiently…

Thread 1

Thread 2

Thread n

…Task 1

Task 2

Task 3

Task 4

Return 1

Callback1()

…and executes each callback as tasks complete

Node app

Page 25: Node.js Patterns for Discerning Developers

Async I/O

The following tasks should be done asynchronously, using the event loop:

• I/O operations• Heavy computation• Anything requiring blocking

Page 26: Node.js Patterns for Discerning Developers

Your Node app is single-threaded

Page 27: Node.js Patterns for Discerning Developers

Anti-pattern: Synchronous Code

for (var i = 0; i < 100000; i++){ // Do anything }

Your app only has one thread, so:

…will bring your app to a grinding halt

Page 28: Node.js Patterns for Discerning Developers

Anti-pattern: Synchronous Code

But why would you do that? Good question.

But in other languages (Python), you may do this:

for file in files:     f = open(file, ‘r’) print f.readline()

Page 29: Node.js Patterns for Discerning Developers

Anti-pattern: Synchronous Code

The Node.js equivalent is:

Based on examples from: https://github.com/nodebits/distilled-patterns/

var fs = require('fs');  for (var i = 0; i < files.length; i++){    data = fs.readFileSync(files[i]); console.log(data); }

…and it will cause severe performance problems

Page 30: Node.js Patterns for Discerning Developers

Pattern: Async I/O

fs = require('fs');  fs.readFile('f1.txt','utf8',function(err,data){     if (err) {        // handle error     }     console.log(data); });

Page 31: Node.js Patterns for Discerning Developers

Async I/O

fs = require('fs');  fs.readFile('f1.txt','utf8',function(err,data){     if (err) {        // handle error     }     console.log(data); });

Anonymous, inline callback

Page 32: Node.js Patterns for Discerning Developers

Async I/O

fs = require('fs');  fs.readFile('f1.txt','utf8', function(err,data){     if (err) {        // handle error     }     console.log(data); } );

Equivalentsyntax

Page 33: Node.js Patterns for Discerning Developers

Callback Hell

When working with callbacks, nesting can get quite out of hand…

Page 34: Node.js Patterns for Discerning Developers

Callback Hell

var db = require('somedatabaseprovider');//get recent postshttp.get('/recentposts', function(req, res) { // open database connection  db.openConnection('host', creds,function(err, conn){    res.param['posts'].forEach(post) {      conn.query('select * from users where id='+post['user'],function(err,users){        conn.close();        res.send(users[0]);      });    }  });});

Page 35: Node.js Patterns for Discerning Developers

Callback Hell

var db = require('somedatabaseprovider');//get recent postshttp.get('/recentposts', function(req, res) { // open database connection  db.openConnection('host', creds,function(err, conn){    res.param['posts'].forEach(post) {      conn.query('select * from users where id='+post['user'],function(err,users){        conn.close();        res.send(users[0]);      });    }  });});

Get recent posts from web service API

Page 36: Node.js Patterns for Discerning Developers

Callback Hell

var db = require('somedatabaseprovider');//get recent postshttp.get('/recentposts', function(req, res) { // open database connection  db.openConnection('host', creds,function(err, conn){    res.param['posts'].forEach(post) {      conn.query('select * from users where id='+post['user'],function(err,users){        conn.close();        res.send(users[0]);      });    }  });});

Open connection to DB

Page 37: Node.js Patterns for Discerning Developers

Callback Hell

var db = require('somedatabaseprovider');//get recent postshttp.get('/recentposts', function(req, res) { // open database connection  db.openConnection('host', creds,function(err, conn){    res.param['posts'].forEach(post) {      conn.query('select * from users where id='+post['user'],function(err,users){        conn.close();        res.send(users[0]);      });    }  });});

Get user from DB for each post

Page 38: Node.js Patterns for Discerning Developers

Callback Hell

var db = require('somedatabaseprovider');//get recent postshttp.get('/recentposts', function(req, res) { // open database connection  db.openConnection('host', creds,function(err, conn){    res.param['posts'].forEach(post) {      conn.query('select * from users where id='+post['user'],function(err,users){        conn.close();        res.send(users[0]);      });    }  });});

Return users

Page 39: Node.js Patterns for Discerning Developers

Callback Hell

var db = require('somedatabaseprovider');//get recent postshttp.get('/recentposts', function(req, res) { // open database connection  db.openConnection('host', creds,function(err, conn){    res.param['posts'].forEach(post) {      conn.query('select * from users where id='+post['user'],function(err,users){        conn.close();        res.send(users[0]);      });    }  });});

Page 40: Node.js Patterns for Discerning Developers

Anti-Pattern: Callback Hellfs.readdir(source, function(err, files) {  if (err) {    console.log('Error finding files: ' + err)  } else {    files.forEach(function(filename, fileIndex) {      console.log(filename)      gm(source + filename).size(function(err, values) {        if (err) {          console.log('Error identifying file size: ' + err)        } else {          console.log(filename + ' : ' + values)          aspect = (values.width / values.height)          widths.forEach(function(width, widthIndex) {            height = Math.round(width / aspect)            console.log('resizing ' + filename + 'to ' + height + 'x' + height)            this.resize(width, height).write(destination+'w’+width+'_’+filename, function(err){              if (err) console.log('Error writing file: ' + err)            })          }.bind(this))        }      })    })  }})

http://callbackhell.com/

Page 41: Node.js Patterns for Discerning Developers

Solutions

• Separate anonymous callback functions (cosmetic)

• Async.js• Promises• Generators

Page 42: Node.js Patterns for Discerning Developers

Pattern: Separate Callbacks

fs = require('fs');  callback = function(err,data){  if (err) {    // handle error   }   console.log(data); }

fs.readFile('f1.txt','utf8',callback);

Page 43: Node.js Patterns for Discerning Developers

Can Turn This var db = require('somedatabaseprovider');

http.get('/recentposts', function(req, res){ db.openConnection('host', creds, function(err,

conn){    res.param['posts'].forEach(post) {      conn.query('select * from users where id=' +

post['user'],function(err,results){        conn.close();        res.send(results[0]);      });    }  });});

Page 44: Node.js Patterns for Discerning Developers

Into This var db = require('somedatabaseprovider');  http.get('/recentposts', afterRecentPosts);  function afterRecentPosts(req, res) {

   db.openConnection('host', creds, function(err, conn) { afterDBConnected(res, conn); }); } function afterDBConnected(err, conn) {   res.param['posts'].forEach(post) {     conn.query('select * from users where id='+post['user'],afterQuery);   } } function afterQuery(err, results) {   conn.close();   res.send(results[0]); }

Page 45: Node.js Patterns for Discerning Developers

This is really a Control Flow issue

Page 46: Node.js Patterns for Discerning Developers

Pattern: Async.js

Async.js provides common patterns for async code control flow

https://github.com/caolan/async

Also provides some common functional programming paradigms

Page 47: Node.js Patterns for Discerning Developers

Serial/Parallel Functions

• Sometimes you have linear serial/parallel computations to run, without branching callback growth

Function 1

Function 2

Function 3

Function 4

Function 1

Function 2

Function 3

Function 4

Page 48: Node.js Patterns for Discerning Developers

Serial/Parallel Functions

async.parallel([    function(){ ... },    function(){ ... }], callback); async.series([    function(){ ... },    function(){ ... }]);

Page 49: Node.js Patterns for Discerning Developers

Serial/Parallel Functions

async.parallel([    function(){ ... },    function(){ ... }], callback); async.series([    function(){ ... },    function(){ ... }], callback);

Single Callback

Page 50: Node.js Patterns for Discerning Developers

Waterfall

Async.waterfall([    function(callback){ ... },    function(input,callback){ ... }, function(input,callback){ ... },], callback);

 

Page 51: Node.js Patterns for Discerning Developers

Map

var arr = ['file1','file2','file3'];  async.map(arr, fs.stat, function(err, results){    // results is an array of stats for each file    console.log('File stats: ' +                  JSON.stringify(results)); });

Page 52: Node.js Patterns for Discerning Developers

Filter

var arr = ['file1','file2','file3'];  async.filter(arr, fs.exists, function(results){    // results is a list of the existing files    console.log('Existing files: ' + results); });

Page 53: Node.js Patterns for Discerning Developers

With great power…

Page 54: Node.js Patterns for Discerning Developers

Carefree

var fs = require('fs');  for (var i = 0; i < 10000; i++) {   fs.readFileSync(filename); }

With synchronous code, you can loop as much as you want:

The file is opened once each iteration.

This works, but is slow and defeats the point of Node.

Page 55: Node.js Patterns for Discerning Developers

Synchronous Doesn’t Scale

What if we want to scale to 10,000+ concurrent users?

File I/O becomes the bottleneck

Users get in a long line

Page 56: Node.js Patterns for Discerning Developers

Async to the Rescue

var fs = require('fs');  function onRead(err, file) {   if (err) throw err; }  for (var i = 0; i < 10000; i++) {   fs.readFile(filename, onRead); }

What happens if I do this asyncronously?

Page 57: Node.js Patterns for Discerning Developers

Ruh Roh

The event loop is fast

This will open the file 10,000 times at once

This is unnecessary…and on most systems, you will run out of file descriptors!

Page 58: Node.js Patterns for Discerning Developers

Pattern: The Request Batch

• One solution is to batch requests• Piggyback on existing requests for

the same file• Each file then only has one open

request at a time, regardless of requesting clients

Page 59: Node.js Patterns for Discerning Developers

// Batching wrapper for fs.readFile() var requestBatches = {}; function batchedReadFile(filename, callback) { // Is there already a batch for this file? if (filename in requestBatches) { // if so, push callback into batch requestBatches[filename].push(callback); return; } // If not, start a new request var callbacks = requestBatches[filename] = [callback]; fs.readFile(filename, onRead); // Flush out the batch on complete function onRead(err, file) { delete requestBatches[filename]; for(var i = 0;i < callbacks.length; i++) { // execute callback, passing arguments along callbacks[i](err, file); } } }

Based on examples from: https://github.com/nodebits/distilled-patterns/

Page 60: Node.js Patterns for Discerning Developers

// Batching wrapper for fs.readFile() var requestBatches = {}; function batchedReadFile(filename, callback) { // Is there already a batch for this file? if (filename in requestBatches) { // if so, push callback into batch requestBatches[filename].push(callback); return; } // If not, start a new request var callbacks = requestBatches[filename] = [callback]; fs.readFile(filename, onRead); // Flush out the batch on complete function onRead(err, file) { delete requestBatches[filename]; for(var i = 0;i < callbacks.length; i++) { // execute callback, passing arguments along callbacks[i](err, file); } } }

Based on examples from: https://github.com/nodebits/distilled-patterns/

Is this file already being read?

Page 61: Node.js Patterns for Discerning Developers

// Batching wrapper for fs.readFile() var requestBatches = {}; function batchedReadFile(filename, callback) { // Is there already a batch for this file? if (filename in requestBatches) { // if so, push callback into batch requestBatches[filename].push(callback); return; } // If not, start a new request var callbacks = requestBatches[filename] = [callback]; fs.readFile(filename, onRead); // Flush out the batch on complete function onRead(err, file) { delete requestBatches[filename]; for(var i = 0;i < callbacks.length; i++) { // execute callback, passing arguments along callbacks[i](err, file); } } }

Based on examples from: https://github.com/nodebits/distilled-patterns/

If not, start a new file read operation

Page 62: Node.js Patterns for Discerning Developers

// Batching wrapper for fs.readFile() var requestBatches = {}; function batchedReadFile(filename, callback) { // Is there already a batch for this file? if (filename in requestBatches) { // if so, push callback into batch requestBatches[filename].push(callback); return; } // If not, start a new request var callbacks = requestBatches[filename] = [callback]; fs.readFile(filename, onRead); // Flush out the batch on complete function onRead(err, file) { delete requestBatches[filename]; for(var i = 0;i < callbacks.length; i++) { // execute callback, passing arguments along callbacks[i](err, file); } } }

Based on examples from: https://github.com/nodebits/distilled-patterns/

When read finished, return to all requests

Page 63: Node.js Patterns for Discerning Developers

Usage

//Request the resource 10,000 times at once for (var i = 0; i < 10000; i++) {   batchedReadFile(file, onComplete); }

function onComplete(err, file) {if (err) throw err;else console.log('File contents: ' + file);

}

Based on examples from: https://github.com/nodebits/distilled-patterns/

Page 64: Node.js Patterns for Discerning Developers

Pattern: The Request Batch

This pattern is effective on many read-type operations, not just file reads

Example: also good for web service API calls

Page 65: Node.js Patterns for Discerning Developers

Shortcomings

Batching requests is great for high request spikes

Often, you are more likely to see steady requests for the same resource

This begs for a caching solution

Page 66: Node.js Patterns for Discerning Developers

Pattern: Request Cache

Let’s try a simple cache

Persist the result forever and check for new requests for same resource

Page 67: Node.js Patterns for Discerning Developers

// Caching wrapper around fs.readFile() var requestCache = {}; function cachingReadFile(filename, callback) { //Do we have resource in cache?   if (filename in requestCache) {      var value = requestCache[filename];     // Async behavior: delay result till next tick     process.nextTick(function () { callback(null, value); });     return;   }    // If not, start a new request   fs.readFile(filename, onRead);    // Cache the result if there is no error   function onRead(err, contents) {     if (!err) requestCache[filename] = contents;     callback(err, contents);  } }

Based on examples from: https://github.com/nodebits/distilled-patterns/

Page 68: Node.js Patterns for Discerning Developers

Usage

// Request the file 10,000 times in series // Note: for serial requests we need to iterate // with callbacks, rather than within a loop var its = 10000; cachingReadFile(file, next);  function next(err, contents) {   console.log('File contents: ' + contents);   if (!(its--)) return;   cachingReadFile(file, next); }

Based on examples from: https://github.com/nodebits/distilled-patterns/

Page 69: Node.js Patterns for Discerning Developers

Almost There!

You’ll notice two issues with the Request Cache as presented:• Concurrent requests are an issue

again• Cache invalidation not handled

Let’s combine cache and batch strategies:

Page 70: Node.js Patterns for Discerning Developers

// Wrapper for both caching and batching of requestsvar requestBatches = {}, requestCache = {};function readFile(filename, callback) {  if (filename in requestCache) { // Do we have resource in cache?    var value = requestCache[filename];    // Delay result till next tick to act async    process.nextTick(function () { callback(null, value); });    return;  }  if (filename in requestBatches) {// Else, does file have a batch?    requestBatches[filename].push(callback);    return;  }  // If neither, create new batch and request  var callbacks = requestBatches[filename] = [callback];  fs.readFile(filename, onRead); // Cache the result and flush batch  function onRead(err, file) {    if (!err) requestCache[filename] = file;    delete requestBatches[filename];    for (var i=0;i<callbacks.length;i++) { callbacks[i](err, file); }  }}

Based on examples from: https://github.com/nodebits/distilled-patterns/

Page 71: Node.js Patterns for Discerning Developers

scale-fs

I wrote a module for scalable File I/O

https://www.npmjs.org/package/scale-fs

Usage:

var fs = require(’scale-fs');  for (var i = 0; i < 10000; i++) {   fs.readFile(filename); }

Page 72: Node.js Patterns for Discerning Developers

Final Thoughts

Most anti-patterns in Node.js come from:

• Sketchy JavaScript heritage• Inexperience with Asynchronous

Thinking

Remember, let the Event Loop do the heavy lifting!

Page 73: Node.js Patterns for Discerning Developers

Thanks

Code samples from this talk at:

https://github.com/cacois/node-patterns-discerning

Page 74: Node.js Patterns for Discerning Developers

Disclaimer

Though I am an employee of the Software Engineering Institute at Carnegie Mellon University, this wok was not funded by the SEI and does not reflect the work or opinions of the SEI or its customers.

Page 75: Node.js Patterns for Discerning Developers

Let’s chat

@aaroncois

www.codehenge.net

github.com/cacois

Page 76: Node.js Patterns for Discerning Developers

Node.js Event Loop

The event loop efficiently manages a thread pool and executes tasks efficiently…

Thread 1

Thread 2

Thread n

…Task 1

Task 2

Task 3

Task 4

Return 1

Callback1()

…and executes each callback as tasks complete

Node.js app

Node apps pass async tasks to the event loop, along with a callback

(function, callback)

1 2

3