Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Microservice in practice with Seneca.js Michele Capra

Microservice in practice with Seneca.js Michele Capra

Microservice architecture is rapidly changing the way we develop our applications. Small, highly decoupled software component are the foundation of this architecture. But hey that’s the theory. Now how can we apply it in our day to day job? How can we structure our application to unleash the benefit of a microservice architecture? Come to my talk and we’ll see an introduction to a real microservice implementation using Seneca.js toolkit. By leveraging plugins, commands and other features available in Seneca.js we’ll see how we can build a microservice application.

Michele Capra

March 19, 2016
Tweet

More Decks by Michele Capra

Other Decks in Technology

Transcript

  1. seneca.js • microservices toolkit for Node.js. • senecajs.org • https://github.com/senecajs/seneca/

    • First commit 21 Oct 2010 • 1100 commits, with 30 contributors
  2. why use seneca • focus on business logic first •

    easy to scale later • clear separation between infrastructure and app code
  3. seneca philosophy • Pattern matching: instead of fragile service discovery,

    you just let the world know what sort of messages you care about • Transport independence: you can send messages between services in many ways, all hidden from your business logic
  4. first microservice var seneca = require('seneca')()! ! seneca.add({role: 'math', cmd:

    ‘sum'},! function (msg, respond) {! var sum = msg.left + msg.right! respond(null, {answer: sum})! ! }! )! ! seneca.act({role:'math',cmd:'sum',left:1,right:2}, ! function (err, result) {! if (err) return console.error(err)! console.log(result)! }! )
  5. first microservice var seneca = require('seneca')()! ! seneca.add({role: 'math', cmd:

    ‘sum'},! function onMatch (msg, respond) {! var sum = msg.left + msg.right! respond(null, {answer: sum})! ! }! )! ! seneca.act({role:'math',cmd:'sum',left:1,right:2}, ! function onResponse (err, result) {! if (err) return console.error(err)! console.log(result)! }! )
  6. first microservice var seneca = require('seneca')()! ! seneca.add({role: 'math', cmd:

    ‘sum'},! function onMatch (msg, respond) {! var sum = msg.left + msg.right! respond(null, {answer: sum})! ! }! )! ! seneca.act({role:'math',cmd:'sum',left:1,right:2}, ! function onResponse(err, result) {! if (err) return console.error(err)! console.log(result)! }! )
  7. first microservice var seneca = require('seneca')()! ! seneca.add({role: 'math', cmd:

    ‘sum'},! function onMatch (msg, respond) {! var sum = msg.left + msg.right! respond(null, {answer: sum})! ! }! )! ! seneca.act({role:'math',cmd:'sum',left:1,right:2}, ! function onResponse (err, result) {! if (err) return console.error(err)! console.log(result)! }! )
  8. add a new microservice seneca.add({role: 'math', cmd: 'product'}, ! function

    onMatch (msg, respond) {! var product = msg.left * msg.right! respond(null, { answer: product })! }! )
  9. all together var seneca = require('seneca')()! ! seneca.add({role: 'math', cmd:

    'sum'}, ! function onMatch (msg, respond) {! var sum = msg.left + msg.right! respond(null, {answer: sum})! })! seneca.add({role: 'math', cmd: 'product'}, ! function onMatch (msg, respond) {! var product = msg.left * msg.right! respond(null, { answer: product })! })! seneca.act({role:'math', cmd:'sum', left:1, right:2}, console.log)! .act({role:'math', cmd:'product', left:3, right:4}, console.log)
  10. extend functionality seneca.add({role: 'math', cmd: 'sum', integer: true}, ! function

    (msg, respond) {! var sum = Math.floor(msg.left) +! Math.floor(msg.right);! respond(null, {answer: sum});! }! )
  11. code re-use seneca.add('role:math, cmd:sum, integer:true',! function (msg, respond) {! this.act({!

    role: 'math',! cmd: 'sum',! left: Math.floor(msg.left),! right: Math.floor(msg.right)! }, respond)! }! )
  12. pattern quick recap • matches against top level properties •

    patterns are unique • specialised patterns win over generic patterns • competing patterns win based on value
  13. plugins of pattern • we can group several patterns into

    a single plugin using a namespacing convention • a plugin can be named • it’s possible to pass a set of initial options to a plugin • usually a plugin is in a single node module
  14. plugins of pattern function math(options) {! ! this.add({role:math,cmd:sum}, ! !

    ! function (msg, respond) ! {! ! ! respond(null, { answer: msg.left + msg.right })! })! ! this.add({role:math,cmd:product}, ! ! ! function (msg, respond) {! ! ! respond(null, { answer: msg.left * msg.right })! })! }! ! require('seneca')()! .use(math)! .act('role:math,cmd:sum,left:1,right:2', console.log)
  15. plugins of pattern function math(options) {! ! this.add('role:math,cmd:sum', ! !

    ! function (msg, respond) ! {! ! ! respond(null, { answer: msg.left + msg.right })! })! ! this.add('role:math,cmd:product', ! ! ! function (msg, respond) {! ! ! respond(null, { answer: msg.left * msg.right })! })! }! ! require('seneca')()! .use(math)! .act('role:math,cmd:sum,left:1,right:2', console.log)
  16. plugins of pattern function math(options) {! ! this.add('role:math,cmd:sum', ! !

    ! function (msg, respond) ! {! ! ! respond(null, { answer: msg.left + msg.right })! })! ! this.add('role:math,cmd:product', ! ! ! function (msg, respond) {! ! ! respond(null, { answer: msg.left * msg.right })! })! }! ! require('seneca')()! .use(math)! .act('role:math,cmd:sum,left:1,right:2', console.log)
  17. seneca web • connect seneca business logic with API •

    integration for both Express and Hapi.js • don’t expose internal pattern to the outside world
  18. express integration module.exports = function api(options) {! ! var valid_ops

    = { sum:'sum', product:'product' }! ! this.add('role:api,path:calculate', function (msg, respond) {! this.act('role:math', {! cmd: valid_ops[msg.operation],! left: msg.left,! right: msg.right,! }, respond)! })! ! this.add('init:api', function (msg, respond) {! this.act('role:web',{use:{! prefix: '/api',! pin: 'role:api,path:*',! map: {! calculate: { GET:true, suffix:'/:operation' }! }! }}, respond)! })! }
  19. express integration module.exports = function api(options) {! ! var valid_ops

    = { sum:'sum', product:'product' }! ! this.add('role:api,path:calculate', ! ! ! function (msg, respond) {! ! ! this.act('role:math', {! ! ! cmd: valid_ops[msg.operation],! ! ! left: msg.left,! ! ! right: msg.right,! ! }, respond)! ! })! . . .! }
  20. express integration module.exports = function api(options) {! ! . .

    .! ! ! this.add('init:api', function (msg, respond) {! ! ! this.act(‘role:web’, { ! ! ! ! use: {! ! ! prefix: '/api',! ! ! pin: 'role:api,path:*',! ! ! map: {! ! ! calculate: { GET:true, suffix:'/:operation' }! ! ! }! ! ! }! ! ! }, respond)! })! }
  21. express integration var seneca = require('seneca')()! .use('api')! .client({ type:'tcp', pin:'role:math'

    })! ! var app = require('express')()! .use(require('body-parser').json())! .use(seneca.export('web'))! .listen(3000)
  22. hapi.js integration • dedicated plugin to do almost the same

    job • https://github.com/hapijs/chairo
  23. storage • seneca provides a simple data abstraction layer, with

    four main operations: load, save, list, remove. • data as json. • in-memory storage by default. • several plugins for the most common databases.
  24. active record style • using seneca.make method you can then

    ! var seneca = require('seneca')()! ! var product = seneca.make('product')! product.name = 'Apple'! product.price = 1.99! ! // sends role:entity,cmd:save,name:product message! product.save$(console.log)
  25. storage example this.add('role:shop,cmd:purchase', function (msg, respond) {! this.make('product').load$(msg.id, function (err,

    product) {! if (err) return respond(err)! ! this! .make('purchase')! .data$({! when: Date.now(),! product: product.id,! name: product.name,! price: product.price,! })! .save$(function (err, purchase) {! if (err) return respond(err)! this.act('role:shop,info:purchase',{purchase:purchase})! respond(null,purchase)! })! })! })
  26. storage plugin list • PostgresSQL • Redis • DynamoDB •

    MySQL • Cassandra • Mongo • SQLite • Hana • CouchDB • LevelDB
  27. project context • Coder Dojo Foundation needed to replace its

    old Zen used for Dojo registration • 860 clubs worldwide in 60 countries
  28. key requirements • Registration for Champions, Mentors and Attendees. •

    Ticket system for events. • Issue Mozilla Open Badges to recognise attendees achievement. • Forums for both mentors and kids. • Reporting for events attendance. • Single sign on for other CoderDojo digital service.
  29. event service • the service looks after the events (ticketing

    for each coding club) section of the API (anything to do with retrieving data/information to do with events).
  30. load-ticket.js 'use strict'; ! function loadTicket (args, callback) { var

    seneca = this; var ticketEntity = seneca.make$('cd/tickets'); var id = args.id; ticketEntity.load$(id, callback); } ! module.exports = loadTicket;
  31. Live projects • coder dojo: https://github.com/CoderDojo/ community-platform • node zoo:

    https://github.com/nodezoo/nodezoo- system • ramanujan: https://github.com/senecajs/ramanujan
  32. take aways • start single process, easily distribute later •

    you’re not locked in • free from architecture dependency • community support (gitter, github)
  33. code - event service ! if (process.env.NEW_RELIC_ENABLED === 'true') require('newrelic');

    ! var config = require('./config/config.js')(); var seneca = require('seneca')(config); var store = require('seneca-postgresql-store'); ! seneca.log.info('using config', JSON.stringify(config, null, 4)); seneca.options(config); ! seneca.use(store, config['postgresql-store']); seneca.use(require('./lib/cd-events')); ! require('./migrate-psql-db.js')(function (err) { if (err) { console.error(err); process.exit(-1); } console.log('Migrations ok'); ! seneca.listen() .client({type: 'web', port: 10301, pin: 'role:cd-dojos,cmd:*'}) .client({type: 'web', port: 10303, pin: 'role:cd-users,cmd:*'}) .client({type: 'web', port: 10303, pin: 'role:cd-profiles,cmd:*'}); });
  34. plugin loading ! var saveEvent = require('./save-event'); var getEvent =

    require(‘./get-event'); . . . var validateSessionInvitation = require('./validate-session-invitation'); var loadTicket = require('./load-ticket'); ! module.exports = function () { var seneca = this; var plugin = 'cd-events'; ! seneca.add({role: plugin, cmd: 'saveEvent'}, saveEvent.bind(seneca)); seneca.add({role: plugin, cmd: 'getEvent'}, getEvent.bind(seneca)); . . . seneca.add({role: plugin, cmd: 'validateSessionInvitation'}, validateSessionInvitation.bind(seneca)); seneca.add({role: plugin, cmd: 'loadTicket'}, loadTicket.bind(seneca)); ! return { name: plugin }; };
  35. save-event.js (271 lines) function saveEvent (args, callback) { var seneca

    = this; var ENTITY_NS = 'cd/events'; ! var eventInfo = args.eventInfo; var plugin = args.role; . . . var emailSubject; var eventSaved; ! async.waterfall([ saveEvent, saveSessions, removeDeletedTickets, emailInvitedMembers ], function (err, res) { if (err) return callback(null, {ok: false, why: err.message}); return callback(null, eventSaved); }); ! }