Slide 1

Slide 1 text

Node.jsでのAWSサーバレスアプリプログラミングを 簡単にする技術の研究紹介 An Introduction of a Technology for Simplifying Serverless Application Programming in AWS with Node.js Kosaku Kimura, FUJITSU LABORATORIES LTD. [email protected] Copyright 2019 FUJITSU LABORATORIES LTD. 0 JAWS DAYS 2019 2019年02月23日 @ TOC五反田メッセ This talk is based on the paper entitled “A JavaScript Transpiler for Escaping from Complicated Usage of Cloud Services and APIs.” in APSEC 2018 Full text (preprint): http://bit.do/escapin_paper

Slide 2

Slide 2 text

Speaker’s Profile Kosaku Kimura(木村 功作)  Researcher at Fujitsu Labs (富士通研究所)  Working on research in Software Engineering  Recent Activities  IEICE(電子情報通信学会),IPSJ(情報処理学会) • IEICE-KBSE(知能ソフトウェア工学研究会) http://www.selab.is.ritsumei.ac.jp/kbse/ • IEICE-SC(サービスコンピューティング研究会) https://sig-sc.org/ • IPSJ-SE(ソフトウェア工学研究会) http://www.ipsj.or.jp/sig/se/  Published research papers continually in academic conferences and workshops • https://www.researchgate.net/profile/Kosaku_Kimura • https://scholar.google.com/citations?user=JB0e924AAAAJ • https://dblp.uni-trier.de/pers/hd/k/Kimura:Kosaku Copyright 2019 FUJITSU LABORATORIES LTD. 1

Slide 3

Slide 3 text

Agenda  Background  Serverless application programming in AWS with Node.js and its difficulties  Escapin: a JavaScript transpiler for escaping from complicated usage of cloud services and APIs  Transpilation Examples  Evaluation  Results  Conclusions Copyright 2019 FUJITSU LABORATORIES LTD. 2

Slide 4

Slide 4 text

An Excerpt of Serverless Application on AWS Copyright 2019 FUJITSU LABORATORIES LTD. Public Web APIs S3 Bucket API Gateway Lambda Function Node.js modules (request, axios, etc.) AWS Cloud IAM 3

Slide 5

Slide 5 text

Lambda Function with Node.js and AWS SDK Copyright 2019 FUJITSU LABORATORIES LTD. import * as AWS from "aws-sdk"; export async function uploadImage(event, context, callback) { const { name, image } = event.formData; const buffer = Buffer.from(image, "binary"); try { await new AWS.S3().putObject({ Bucket: "images", Key: name, Body: buffer }).promise(); callback(null, { message: "Succeeded" }); } catch (err) { callback(err); } } ※For the program uses ES Modules, we need a polyfill to deploy it to Lambda 4

Slide 6

Slide 6 text

Asynchronous Programming in JavaScript Copyright 2019 FUJITSU LABORATORIES LTD. Error-first callback Promise Promisify func(args, (err, data) => { if (err) handleError(err); else doSomething(data); }); const p = args => { return new Promise((resolve, reject) => { func(args, (err, data) => { if (err) reject(err); else resolve(data); }); }); }; p(args).then(doSomething).catch(handleError); import { promisify } from "util"; const p = promisify(func); p(args).then(doSomething).catch(handleError); 5

Slide 7

Slide 7 text

Asynchronous Programming in JavaScript Copyright 2019 FUJITSU LABORATORIES LTD. async / await generator / yield import co from "co"; const p = promisify(func); co(function* () { const data = yield p(args); doSomething(data); }).catch(handleError); const p = promisify(func); (async () => { try { const data = await p(args); doSomething(data); } catch (err) { handleError(err); } })(); ? 6

Slide 8

Slide 8 text

Welcome to async/await hell Copyright 2019 FUJITSU LABORATORIES LTD. “async/await freed us from callback hell, but people have started abusing it — leading to the birth of async/await hell.” — Aditya Agarwal (async () => { const pizzaData = await getPizzaData() // async call const drinkData = await getDrinkData() // async call const chosenPizza = choosePizza() // sync call const chosenDrink = chooseDrink() // sync call await addPizzaToCart(chosenPizza) // async call await addDrinkToCart(chosenDrink) // async call orderItems() // async call })() async function selectPizza() { const pizzaData = await getPizzaData() // async call const chosenPizza = choosePizza() // sync call await addPizzaToCart(chosenPizza) // async call } async function selectDrink() { const drinkData = await getDrinkData() // async call const chosenDrink = chooseDrink() // sync call await addDrinkToCart(chosenDrink) // async call } (async () => { const pizzaPromise = selectPizza() const drinkPromise = selectDrink() await pizzaPromise await drinkPromise orderItems() // async call })() async function orderItems() { const items = await getCartItems() // async call const noOfItems = items.length for(var i = 0; i < noOfItems; i++) { await sendRequest(items[i]) // async call } } async function orderItems() { const items = await getCartItems() // async call const noOfItems = items.length const promises = [] for(var i = 0; i < noOfItems; i++) { const orderPromise = sendRequest(items[i]) // async call promises.push(orderPromise) // sync call } await Promise.all(promises) // async call } async function orderItems() { const items = await getCartItems() // async call const promises = items.map((item) => sendRequest(item)) await Promise.all(promises) // async call } OR 7

Slide 9

Slide 9 text

import { resize } from "imagemagick"; import rp from "request-promise-native"; import * as AWS from "aws-sdk"; export async function uploadImage(event, context, callback) { const { name, image } = event.formData; const buffer = Buffer.from(image, "binary"); await new AWS.S3().putObject({ Bucket: "images", Key: name, Body: buffer }).promise(); resize({ srcData: buffer, format: "jpeg", width: 100 }, (err, stdout, stderr) => { if (err) { callback(err); return; } const data = Buffer.from(stdout, "binary"); rp({ url: `https://api.com/v1/thumbnails/${name}`, method: "put", formData: { name, data }, headers: { "content-type": "multipart/form-data" } }).then(() => { callback(null, { message: "Succeeded" }); }).catch(err => { throw err; }); }); } Serverless program must be complicated and error-prone even if you believe what you want is simple Copyright 2019 FUJITSU LABORATORIES LTD. api.com images API Gateway uploadImage Node.js modules AWS Cloud IAM 8

Slide 10

Slide 10 text

Wouldn’t it be nice if you could write like this? Copyright 2019 FUJITSU LABORATORIES LTD. import api from "https://api.com/v1/spec"; import { resize } from "imagemagick"; export const images = {}; export function uploadImage(event) { const { name, image } = event.formData; const buffer = Buffer.from(image, "binary"); images[name] = buffer; try { const { stdout, stderr } = resize({ srcData: buffer, format: "jpeg", width: 100 }); const data = Buffer.from(stdout, "binary"); api.thumbnails[name] = { name, data }; return { message: "Succeeded" }; } catch (err) { throw err; } } api.com images API Gateway uploadImage AWS Cloud IAM Node.js modules 9

Slide 11

Slide 11 text

1. A JavaScript Transpiler for Escaping from Complicated Usage of Cloud Services and APIs 2. ECMAScript (JavaScript) for Cloud and API Nature Escapin Copyright 2019 FUJITSU LABORATORIES LTD. 10

Slide 12

Slide 12 text

Key Points  RESTful API calls just like imported objects  S3/DynamoDB access just like exported hash tables  Lambda function declarations just like exported functions  Asynchronous features are complemented when necessary (Callback-agnostic programming)  The above points never extend JavaScript syntax Copyright 2019 FUJITSU LABORATORIES LTD. import api from "https://api.com/v1/spec"; import { resize } from "imagemagick"; export const images = {}; export function uploadImage(event) { const { name, image } = event.formData; const buffer = Buffer.from(image, "binary"); images[name] = buffer; try { const { stdout, stderr } = resize({ srcData: buffer, format: "jpeg", width: 100 }); const data = Buffer.from(stdout, "binary"); api.thumbnails[name] = { name, data }; return { message: "Succeeded" }; } catch (err) { throw err; } } 11

Slide 13

Slide 13 text

Key Points  RESTful API calls just like imported objects  S3/DynamoDB access just like exported hash tables  Lambda function declarations just like exported functions  Asynchronous features are complemented when necessary (Callback-agnostic programming)  The above points never extend JavaScript syntax Copyright 2019 FUJITSU LABORATORIES LTD. import api from "https://api.com/v1/spec"; api.thumbnails[name] = { name, data }; 12

Slide 14

Slide 14 text

Key Points  RESTful API calls just like imported objects  S3/DynamoDB access just like exported hash tables  Lambda function declarations just like exported functions  Asynchronous features are complemented when necessary (Callback-agnostic programming)  The above points never extend JavaScript syntax Copyright 2019 FUJITSU LABORATORIES LTD. export const images = {}; images[name] = buffer; 13

Slide 15

Slide 15 text

Key Points  RESTful API calls just like imported objects  S3/DynamoDB access just like exported hash tables  Lambda function declarations just like exported functions  Asynchronous features are complemented when necessary (Callback-agnostic programming)  The above points never extend JavaScript syntax Copyright 2019 FUJITSU LABORATORIES LTD. export function uploadImage(event) { return { message: "Succeeded" }; throw err; } 14

Slide 16

Slide 16 text

Key Points  RESTful API calls just like imported objects  S3/DynamoDB access just like exported hash tables  Lambda function declarations just like exported functions  Asynchronous features are complemented when necessary (Callback-agnostic programming)  The above points never extend JavaScript syntax Copyright 2019 FUJITSU LABORATORIES LTD. images[name] = buffer; const { stdout, stderr } = resize({ srcData: buffer, format: "jpeg", width: 100 }); api.thumbnails[name] = { name, data }; } 15

Slide 17

Slide 17 text

Key Points  RESTful API calls just like imported objects  S3/DynamoDB access just like exported hash tables  Lambda function declarations just like exported functions  Asynchronous features are complemented when necessary (Callback-agnostic programming)  The above points never extend JavaScript syntax Copyright 2019 FUJITSU LABORATORIES LTD. import api from "https://api.com/v1/spec"; import { resize } from "imagemagick"; export const images = {}; export function uploadImage(event) { const { name, image } = event.formData; const buffer = Buffer.from(image, "binary"); images[name] = buffer; try { const { stdout, stderr } = resize({ srcData: buffer, format: "jpeg", width: 100 }); const data = Buffer.from(stdout, "binary"); api.thumbnails[name] = { name, data }; return { message: "Succeeded" }; } catch (err) { throw err; } } 16

Slide 18

Slide 18 text

Type Information Transpilation Procedure const items = {}; ... items[id] = obj; ... ... await new Promise( (resolve, reject) => { Storage.put({ name: "items", key: id, value: obj }, (err, res) =>{ if (err) reject(err); else resolve(res); }); ... Transformation Rules platform: a-cloud where empty object $1 is declared && statement matches /$1[$2] = $3;/ when platform == "a-cloud" do replace statement with Storage.put({ name: `${$1}`, key: $2, value: $3 }); PIP PSP for setup Configuration File function function type Storage#put error-first-callback switch (process.argv[2]) { case "create": Storage.createTable({ name: "items", ... }); ret = IAM.createRole( { ... }); FaaS.createFunction({ FunctionName: "itemsGET", Role: ret.Role, ... }); break; case "update": ... PSP to be deployed as a function Destructuring Nesting Callbacks Refinements Asynchronization Copyright 2019 FUJITSU LABORATORIES LTD.  From platform-independent programs (PIPs) to platform-specific programs (PSPs) having the same functionality but utilize Clouds and APIs in order to satisfy requirements 17

Slide 19

Slide 19 text

Destructuring Nesting Callbacks Obtain a complete callback-agnostic representation from a PIP that has legacy nesting callbacks Copyright 2019 FUJITSU LABORATORIES LTD. func(arg, (err, data) => { if (err) { handleError(err); return; } doSomething(data); }); try { const data = func(arg); doSomething(data); } catch (err) { handleError(err); } 18

Slide 20

Slide 20 text

Refinements – APIs Method Path GET POST PUT DELETE /items api.items api.items(body) api.items = body delete api.items /items/:id api.items[id] api.items[id](body) api.items[id] = body delete api.items[id] /items/:id with params api.items [id][params] api.items [id][params](body) api.items [id][params] = body delete api.items [id][params]  Manipulations of objects imported from a URI of an API specification file, which conforms to OpenAPI Specification, are transformed into corresponding API calls Copyright 2019 FUJITSU LABORATORIES LTD. import api from 'https://path/to/swagger.yaml'; const item = api.items[id]; const item = request({ 'uri': `https://api.com/v1/items/${id}`, 'method': 'get', 'contentType': 'application/json', 'headers': { 'authorization': 'Basic ... ' } }); 19

Slide 21

Slide 21 text

Function Lambda export function foo(bar) {…} createFunction Refinements – AWS Object DynamoDB S3 export const obj = {} createTable createBucket obj[id] getItem getObject obj[id] = … putItem putObject delete obj[id] deleteItem deleteObject Object.keys(obj) scan listObjectV2  Manipulations of exported objects and functions are transformed into corresponding API calls for cloud services Copyright 2019 FUJITSU LABORATORIES LTD. 20

Slide 22

Slide 22 text

Refinements – AWS platform: aws PIP PSP for setup Configuration File PSP to be deployed Copyright 2019 FUJITSU LABORATORIES LTD. export const csv = {}; const keys = Object.keys(csv); const data = new AWS.DynamoDB().scan({ TableName: 'csv', … }); const keys = data.Items.map( item => item.key.S); import AWS from 'aws-sdk'; switch (process.argv[2]) { case 'create': new AWS.DyanmoDB().createTable({ TableName: 'csv', …}); case 'update': … 21

Slide 23

Slide 23 text

Asynchronization Function Type DynamoDB#scan error-first-callback Array#map general-callback … … PSP Asynchronized PSP Type Information Copyright 2019 FUJITSU LABORATORIES LTD. const data = new AWS.DynamoDB().scan({ TableName: 'csv', … }); const keys = data.Items.map( item => item.key.S); const _data = await new Promise((resolve, reject) => { new AWS.DynamoDB().scan({ TableName: 'csv', … }, (err, data) => { if (err) reject(err); else resolve(data); }); }); const keys = _data.Items.map( item => item.key.S); 22

Slide 24

Slide 24 text

Transpilation Examples Copyright 2019 FUJITSU LABORATORIES LTD. 23

Slide 25

Slide 25 text

Functions with Error-first Callback Copyright 2019 FUJITSU LABORATORIES LTD. func(args, (err, data) => { if (err) handleError(err); else doSomething(data); }); try { const _data = await new Promise( (resolve, reject) => { func(args, (err, data) => { if (err) reject(err); else resolve(data); }); }); doSomething(_data); } catch (err) { handleError(err); } try { const data = func(arg); doSomething(data); } catch (err) { handleError(err); } OR 24

Slide 26

Slide 26 text

API Calls Copyright 2019 FUJITSU LABORATORIES LTD. import api from 'https://path/to/swagger.yaml'; if (api.items[id].attr === "foo") { doSomething(api.items[id].attr); } const _data = await new Promise( (resolve, reject) => { request({ 'uri': `https://api.com/v1/items/${id}`, 'method': 'get', 'contentType': 'application/json', 'headers': { 'authorization': 'Basic ... ' } }, (err, data) => { if (err) reject(err); else resolve(data); }); }); if (_data.attr === "foo") { doSomething(_data.attr); } 25

Slide 27

Slide 27 text

Iterations Copyright 2019 FUJITSU LABORATORIES LTD. for (let i = 0; i < n; i++) { asyncFunc(i); } const _p = []; for (let i = 0; i < n; i++) { _p.push(asyncFunc(i)); } await Promise.all(_p); for (const item of items) { const data = asyncFunc(item); doSomething(data); } const _promises = []; for (const item of items) { _promises.push((async () => { const data = await asyncFunc(item); doSomething(data); })()); } await Promise.all(_promises); 26

Slide 28

Slide 28 text

Functions with Callback in which async/await cannot be used Copyright 2019 FUJITSU LABORATORIES LTD. func(item => { const ret = asyncFunc(item); doSomething(ret); }); import deasync from "deasync"; func(item => { let _t, _done = false; asyncFunc(item).then(data => { _t = data; }).catch(err => { throw err; }).finally(() => { _done = true; }); deasync.loopWhile(() => !_done); const ret = _t; doSomething(ret); }); 27

Slide 29

Slide 29 text

A Complicated Case with DynamoDB Copyright 2019 FUJITSU LABORATORIES LTD. export const items = {}; if (Object.keys(items).length > 0) { doSomething(items[foo]); } else if (asyncFunc(args)) { doSomething(bar); } const _data = await new Promise((resolve, reject) => { new AWS.DynamoDB().scan({ TableName: 'items', ExpressionAttributeNames: { '#ky': 'key', }, ProjectionExpression: '#ky', }, (err, data) => { if (err) reject(err); else resolve(data); }); }); const keys = _data.Items.map(item => item.key.S); if (keys.length > 0) { let _data2; const _data3 = await new Promise((resolve, reject) => { new AWS.DynamoDB().getItem({ TableName: 'items', Key: { key: { S: foo, }, }, }, (err, data) => { if (err) reject(err); else resolve(data); }); }); if (_data3 === null || _data3.Item === undefined) { _data2 = undefined; } else { _data2 = (_data3.Item.type.S === 'object' || _data3.Item.type.S === 'function') ? JSON.parse(_data3.Item.value.S) : _data3.Item.value.S; } doSomething(_data2); } else { const _data3 = await asyncFunc(args); if (_data3) { doSomething(bar); } } 28

Slide 30

Slide 30 text

Evaluation Copyright 2019 FUJITSU LABORATORIES LTD. 29

Slide 31

Slide 31 text

Research Questions RQ1: (Effectiveness) To what degree does Escapin reduce the amount of time to develop applications using APIs and cloud services? RQ2: (Source code simplicity) To what degree can engineers write simple source code by using Escapin? Copyright 2019 FUJITSU LABORATORIES LTD. 30

Slide 32

Slide 32 text

Preliminary Implemented Escapin as services  using Node.js, Yarn, Babel, Webpack, TypeScript, SwaggerParser  CLI and documentation also available Created two API servers on AWS by using Escapin  Mock server of Uber API implementing price estimation feature only  Test API for checking whether applications are correctly implemented Gathered 10 subjects  researchers and software engineers in Fujitsu. Copyright 2019 FUJITSU LABORATORIES LTD. 31

Slide 33

Slide 33 text

Development time = Σ len( ) Experimental Design (1/2)  Subjects do an application development task using the mock Uber API to confirm:  for RQ1, development time  for RQ2, lines of code (LOC), cyclomatic complexity, and cognitive complexity  Before doing the task, they learn how to use AWS, Node.js, and Escapin enough to remove the learning time from the evaluation  Measure development time by requesting Test API in order to do the task at subjects’ own workspaces start(null) → false submit v1 → false suspend(null) → false resume(null) → false submit v2 → true submit v3 → true t Copyright 2019 FUJITSU LABORATORIES LTD. 32

Slide 34

Slide 34 text

Experimental Design (2/2)  All subjects do the task in two cases in order to compare metrics for each subject independently  Case 1: using any existing tools without Escapin  Case 2: using Escapin  Separate the ten subjects into two groups that do the task in different order of the cases  for fair evaluation against the existence of common operations whose artifacts can be reused in a latter case (e.g., finding correct response parameters of Uber API) ×10 ×5 ×5 Group 1 Group 2 Case 2 Case 1 Case 1 Case 2 Copyright 2019 FUJITSU LABORATORIES LTD. 33

Slide 35

Slide 35 text

Task: Summarizing Estimations of Uber Products Develop an API GET /rough_estimates conforming to the following API specification:  The API is deployed on AWS API Gateway and Lambda (Node.js)  The API has four query parameters: • start_latitude, start_longitude, end_latitude, and end_longitude  The API conducts the following steps: 1. Get estimate and product_id of an available car nearby by requesting GET /estimates/time with the query parameters. 2. Rename estimate to time. 3. Get the car information description, capacity and image by requesting GET /estimates/:id with product_id obtained by step 2. 4. Get price estimation information by requesting GET /estimates/price with the query parameters. 5. From the information obtained by step 4, get estimate and distance that correspond to each product_id obtained by step 2. 6. Exclude entries that estimate is set to "Metered", which means this is a taxi. 7. Rename estimate to price. 8. Format the data as a JSON object and return it. Copyright 2019 FUJITSU LABORATORIES LTD. 34

Slide 36

Slide 36 text

Results Copyright 2019 FUJITSU LABORATORIES LTD. 35

Slide 37

Slide 37 text

Subject A bit.ly/escapin-results 415 min 210 min Case 1 Case 2 Copyright 2019 FUJITSU LABORATORIES LTD. 36

Slide 38

Slide 38 text

Subject B 200 min 110 min Case 1 Case 2 bit.ly/escapin-results Copyright 2019 FUJITSU LABORATORIES LTD. 37

Slide 39

Slide 39 text

Subject C 81 min 66 min Case 1 Case 2 bit.ly/escapin-results Copyright 2019 FUJITSU LABORATORIES LTD. 38

Slide 40

Slide 40 text

Subject D 175 min 82 min Case 1 Case 2 bit.ly/escapin-results Copyright 2019 FUJITSU LABORATORIES LTD. 39

Slide 41

Slide 41 text

Subject E 127 min 69 min Case 1 Case 2 bit.ly/escapin-results Copyright 2019 FUJITSU LABORATORIES LTD. 40

Slide 42

Slide 42 text

Subject F 226 min 175 min Case 1 Case 2 bit.ly/escapin-results Copyright 2019 FUJITSU LABORATORIES LTD. 41

Slide 43

Slide 43 text

Subject G 309 min 168 min Case 1 Case 2 bit.ly/escapin-results Copyright 2019 FUJITSU LABORATORIES LTD. 42

Slide 44

Slide 44 text

Subject H 167 min 97 min Case 1 Case 2 bit.ly/escapin-results Copyright 2019 FUJITSU LABORATORIES LTD. 43

Slide 45

Slide 45 text

Subject I 329 min 182 min Case 1 Case 2 bit.ly/escapin-results Copyright 2019 FUJITSU LABORATORIES LTD. 44

Slide 46

Slide 46 text

Subject J 299 min 165 min Case 1 Case 2 bit.ly/escapin-results Copyright 2019 FUJITSU LABORATORIES LTD. 45

Slide 47

Slide 47 text

Answer to the Research Questions  Escapin statistically significantly (α=0.05)  decreases development time (RQ1)  simplifies source code (RQ2) RQ1 Effectiveness RQ2 Source code simplicity Development Time LOC Cyclomatic Complexity Cognitive Complexity p-value 0.00016 0.00003 0.00181 0.01384 Effect Size (Cohen’s d) Huge (11.66398) Huge (9.61006) Huge (5.04810) Huge (2.47486) Observed Statistical Power 0.99179 0.99995 0.99994 0.90048 by paired-sample one-tailed t-test Copyright 2019 FUJITSU LABORATORIES LTD. 46

Slide 48

Slide 48 text

Threats to Validity Threats to internal validity  The number of subjects (n=10) is too small to confirm whether the all metrics follow a normal distribution  Correctness of measuring development time by manually requesting Test API  Overhead of context switching caused by taking breaks Threats to external validity  Task is too small and few to remark the generality and applicability Copyright 2019 FUJITSU LABORATORIES LTD. 47

Slide 49

Slide 49 text

Conclusions  Escapin: a JavaScript transpiler for escaping from complicated usage of cloud services and APIs  Introduce new features without extending JavaScript syntax • Importing APIs as objects • Exporting objects and functions as cloud service resources  Enable callback-agnostic programming • No need to use asynchronous features in programming  In the application development task using Uber API, Escapin decreases development time and simplifies source code compared with existing tools Future work  Further evaluation  more complicated tasks  a number of subjects enough to do interval estimation of the metrics  Support cloud services other than AWS  Provide auxiliary tools: linter, code completion, testing, debugging, etc.  Transpiling optimizations for satisfying non-functional requirements Copyright 2019 FUJITSU LABORATORIES LTD. 48

Slide 50

Slide 50 text

No content