Built on Seneca and PM2
This chapter is divided into three sections:
- Why you choose Nodejs: This will prove the correctness of choosing Node.js to build. Describes the software stack designed when using Node.js.
- Microservices Architecture Seneca: Basics about Seneca.
- PM2: PM2 is the best choice for running node.js applications.
Reasons for choosing Node.js
Node.js is now the solution of choice for many international technology companies. In particular, node.js seems to be the best choice for scenarios that require a blocking feature on the server side.
In this chapter we focus on Seneca and PM2 as frameworks for building and running microservices. Just because you chose Seneca and PM2 doesn’t mean the other frameworks are bad.
There are other options available in the industry, such as Restify or Express, Egg. Js can be used to build applications, and Forever or Nodemon can be used to run applications. Seneca and PM2, in my opinion, are the best combination to build microservices for the following reasons:
- PM2 has exceptionally powerful functions in application deployment.
- Seneca is more than just an architecture for building services; it’s a paradigm that will reshape our understanding of object-oriented software.
The first program – Hello World
One of the most exciting ideas in Node.js is simplicity. Once you’re familiar with JavaScript, you can learn Node.js in a few days. Code written in Node.js is shorter than code written in other languages:
const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) = > {
res.statusCode = 200;
res.setHeader('Content-Type'.'text/plain');
res.end('Hello World\n');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/ `);
});
Copy the code
The above code creates a server program and listens on port 3000. After running the code, you can enter: http://127.0.0.1:3000 in the browser to preview Hello World.
Node.js threading model
Node.js uses an asynchronous processing mechanism. This means that when a slow event, such as reading a file, is processed, node.js does not block the thread, but continues to process other events, and the Noede. Js control flow executes methods to process the returned information when the file is read.
As an example of the code in the above section, the http.createserver method accepts a callback function that will be executed when an HTTP request is received. But while waiting for the HTTP request, the thread can still process other events.
SOLID design principles
When we talk about microservices, we always talk about modularity, and modularity comes down to the following design principles:
- Single responsibility principle
- Open closed principle (open for extension, closed for modification)
- The Richter substitution principle (if you use a superclass, it must apply to its subclasses, and you can’t tell the difference between a superclass and a subclass. In other words, if you replace a parent class with a child class, the behavior will not change. Simply put, subtypes must be able to replace their parent types.
- Interface separation Principle
- Dependency Inversion Principle (Inversion of control and dependency Injection)
You should organize your code in modules. A module should be an aggregation of code that handles something simply and well, such as manipulating strings. Note, however, that the more functions (classes, tools) your module contains, the less cohesive it will be, which should be avoided at all costs.
In Node.js, each JavaScript file is a module by default. Of course, you can organize modules in folders as well, but for now we’ll only focus on using files:
function contains(a, b) {
return a.indexOf(b) > - 1;
}
function stringToOrdinal(str) {
let result = ' ';
for (let i = 0, len = str.length; i < len; i++) {
result += charToNuber(str[i]);
}
return result;
}
function charToNuber(char) {
return char.charCodeAt(0) - 96;
}
module.exports = {
contains,
stringToOrdinal
}
Copy the code
The above code is a valid Node.js module. These three modules have three functions, two of which are used as common functions to expose external modules.
To use this module, simply require(), as follows:
const stringManipulation = request('./string-manipulation');
console.log(stringManipulation.stringToOrdinal('aabb'));
Copy the code
The output is 1122.
In light of the SOLID principle, review our modules.
- Single design principle: Modules only deal with strings.
- The open and closed principle (open for extension, closed for modification) : more functions can be added to a module, and existing, correct functions can be used to build new functions in the module, while we don’t modify common code.
- Richter’s replacement rule: Skip this rule because the structure of the module does not embody this rule.
- Interface separation principle:Unlike Java and C#, JavaScript is not a pure interface-oriented language. But this module does expose the interface. through
module.exports
Variables expose the interface of a common function to the caller so that changes to the implementation do not affect the coding of the consumer. - Dependency inversion: This is a failure, not a complete failure, but enough that we have to rethink our approach.
The microservices framework Seneca
Seneca is a enables you to quickly build a micro service system based on message tool set, you don’t need to know all kinds of service itself is deployed in where, don’t need to know how many services exist, also do not need to know what is it exactly that they do, any service outside of your business logic (such as database and cache or a third party integration, etc.) are hidden in after service.
This decoupling makes your system easy to build and update continuously, which Seneca does because of its three core features:
- Pattern matching: Unlike fragile service discovery, pattern matching is designed to tell the world what messages you really care about;
- Dependent transport: You can send messages between services in a variety of ways, all hidden behind your business logic;
- Componentization: Functionality is represented as a set of plug-ins that can work together to form microservices.
In Seneca, a message is a JSON object that can have any internal structure you like. It can be transmitted via HTTP/HTTPS, TCP, message queues, publish/subscribe services, or any other way to transmit data. For you as the message producer, you just send the message out. There is no need to care which services receive them.
Then, you want to tell the world that you want to receive some messages, which is as simple as doing a little matching pattern configuration in Seneca. The matching pattern is as simple as a list of key and value pairs that are used to match the polar group properties of the JSON message.
In the rest of this article, we’ll work together to build some microservices based on Seneca.
Patterns
Let’s start with a bit of very simple code. We’ll create two microservices, one that does the math and one that calls it:
const seneca = require('seneca') (); seneca.add('role:math, cmd:sum', (msg, reply) => {
reply(null, { answer: ( msg.left + msg.right )})
});
seneca.act({
role: 'math'.cmd: 'sum'.left: 1.right: 2
}, (err, result) => {
if (err) {
return console.error(err);
}
console.log(result);
});
Copy the code
Currently, it all happens in the same process, with no network traffic. An in-process function call is also a message transfer!
The seneca.add method adds a new mode of action to the Seneca instance. It takes two arguments:
- Pattern: The property pattern to match in any JSON message received by a Seneca instance.
- Action: The function to execute when the pattern matches the message.
The action function has two parameters:
- MSG: The matching inbound message (provided as a normal object).
- Respond: a callback function that provides a response to a message.
The response function is a callback function with the standard ERROR, result signature.
Let’s put it all together again:
seneca.add({role: 'math'.cmd: 'sum'}, function (msg, respond) {
var sum = msg.left + msg.right
respond(null, {answer: sum})
})
Copy the code
In the sample code, the operation calculates the sum of two numbers provided by the left and right properties of the message object. Not all messages generate results, but since this is the most common case, Seneca allows you to provide results through callback functions.
In summary, the operation modes Role :math, CMD :sum work on this message:
{role: 'math', cmd: 'sum', left: 1, right: 2}
Copy the code
Produce this result:
{answer: 3}
Copy the code
There is nothing special about CMD about these attribute roles. They are exactly the ones you use for pattern matching.
The seneca.act method submits the message for action. It takes two arguments:
- MSG: message object.
- Response_callback: a function that receives a response to a message, if any.
The response callback is what you provide with the standard ERROR, result signature. If there is a problem (for example, the message does not match any pattern), the first argument is an Error object. If all goes according to plan, the second parameter is the result object. In the sample code, these parameters are just printed to the console:
seneca.act({role: 'math'.cmd: 'sum'.left: 1.right: 2}, function (err, result) {
if (err) return console.error(err)
console.log(result)
})
Copy the code
The sample code in the sum.js file shows you how to define and invoke the mode of action in the same Node.js process. You’ll soon see how to split this code across multiple processes.
How does the matching pattern work?
Patterns – as opposed to network addresses or topics – make it easier to extend and enhance a system. They do this by gradually adding new microservices.
Let’s increase the ability of our system to multiply two numbers.
We want the news to look like this:
{role: 'math', cmd: 'product', left: 3, right: 4}
Copy the code
To produce such results:
{answer: 12}
Copy the code
You can use the ROLE: Math, CMD: sum operation mode as a template to define a new role: math, CMD: product operation:
seneca.add({role: 'math'.cmd: 'product'}, function (msg, respond) {
var product = msg.left * msg.right
respond(null, { answer: product })
})
Copy the code
You can call it in exactly the same way:
seneca.act({role: 'math'.cmd: 'product'.left: 3.right: 4}, console.log)
Copy the code
From here, you can use the console.log shortcut to print the errors (if any) and the results. Running this code produces:
{answer: 12}
Copy the code
Put it all together and you get:
var seneca = require('seneca')()
seneca.add({role: 'math'.cmd: 'sum'}, function (msg, respond) {
var sum = msg.left + msg.right
respond(null, {answer: sum})
})
seneca.add({role: 'math'.cmd: 'product'}, function (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)
Copy the code
In the code example above, the seneca.act calls are linked together. Seneca provides a link API as a convenience. Linked calls are executed sequentially, but not sequentially, so their results can be returned in any order.
Extend the mode to add new features
Patterns let you easily extend functionality. Instead of adding if statements and complex logic, you just need to add more patterns.
Let’s extend the addition action by adding the ability to force integer operations. To do this, you need to add a new attribute, INTEGER: true, to the message object. Then, provide a new action for messages that have this property:
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})
})
Copy the code
Now, this news
{role: 'math', cmd: 'sum', left: 1.5, right: 2.5, integer: true}
Copy the code
Produce this result:
{answer: 3} // == 1 + 2, as decimals removed
Copy the code
What happens if you add both modes to the same system? How does Seneca choose which one to use? The more specific pattern always wins. In other words, the pattern with the most matching properties takes precedence.
Here’s some code to illustrate this:
var seneca = require('seneca')()
seneca.add({role: 'math'.cmd: 'sum'}, function (msg, respond) {
var sum = msg.left + msg.right
respond(null, {answer: sum})
})
// The following two messages match role: math, CMD: sum
seneca.act({role: 'math'.cmd: 'sum'.left: 1.5.right: 2.5}, console.log)
seneca.act({role: 'math'.cmd: 'sum'.left: 1.5.right: 2.5.integer: true}, console.log)
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 })
})
// The following message matches role: math, CMD: sum
seneca.act({role: 'math'.cmd: 'sum'.left: 1.5.right: 2.5}, console.log)
/ / but also match the role: math, CMD: the sum of the integer: true
// But since more attributes are matched, it has a higher priority
seneca.act({role: 'math'.cmd: 'sum'.left: 1.5.right: 2.5.integer: true}, console.log)
Copy the code
The output it produces is:
2016... INFO hello ... null { answer: 4 } null { answer: 4 } null { answer: 4 } null { answer: 3 }Copy the code
The first two. Act calls match the role: math, CMD: sum action pattern. Next, the code defines only integer action patterns role: math, CMD: sum, and INTEGER: true. After that, the third call to. Act acts the same as role: math, CMD: sum, but the fourth call to role: math, CMD: sum, integer: true. This code also demonstrates that you can link.add and.act calls. This code is available in the sum-integer.js file.
The ability to easily extend the behavior of operations by matching more specific message types is an easy way to handle new and changing requirements. This applies both to projects where your project is under development and to projects where you are in real time and need to adapt. It also has the advantage that you don’t need to modify existing code. It’s safer to add new code to handle special cases. In a production system, you don’t even need to redeploy. Your existing services can run as they are. All you need to do is start the new service.
Pattern-based code reuse
Action modes can call other action modes to do their job. Let’s modify our sample code to use this method:
var seneca = require('seneca')()
seneca.add('role: math, cmd: sum'.function (msg, respond) {
var sum = msg.left + msg.right
respond(null, {answer: sum})
})
seneca.add('role: math, cmd: sum, integer: true'.function (msg, respond) {
Role :math, CMD :sum
this.act({
role: 'math'.cmd: 'sum'.left: Math.floor(msg.left),
right: Math.floor(msg.right)
}, respond)
})
/ / match role: math, CMD: sum
seneca.act(Role: math, CMD: sum, left: 1.5, right: 2.5'.console.log)
/ / match role: math, CMD: the sum of the integer: true
seneca.act('role: math, cmd: sum, left: 1.5, right: 2.5, integer: true'.console.log)
Copy the code
In this version of the code, the role: Math, CMD: sum, INTEGER: true operation mode is defined using the previously defined Role: math, CMD: sum operation mode. However, it first modifies the message to convert the left and right properties to integers.
Inside the action function, the context variable this is a reference to the current Seneca instance. This is the correct way to reference Seneca in an action, because you get the full context of the current action invocation. This makes your logs more informative and so on.
This code uses an abbreviated FORM of JSON to specify the schema and message. For example, object literal form
{role: 'math', cmd: 'sum', left: 1.5, right: 2.5}
Copy the code
To:
Role: math, CMD: sum, left: 1.5, right: 2.5'
Copy the code
This format, JSonic, provided as string literals, is a convenient format to make patterns and messages in your code more concise.
The code for the above example is provided in the sum-reservice.js file.
The pattern is unique
The Action patterns you define are unique. They can only trigger one function. The pattern is resolved as follows:
- More me attributes have higher priority
- If the patterns have the same number of attributes, they are matched alphabetically
Here are some examples:
A :1, b:2 takes precedence over a:1 because it has more properties.
A :1, B :2 takes precedence over A :1, C :3
A :1, B :2, D :4, a:1, C :3, D :4
A :1, b:2, c:3 takes precedence over a:1, b:2 because it has more properties.
A :1, B :2, c:3 takes precedence over a:1, C :3 because it has more properties.
A lot of time, can provide a let you don’t need to completely change the existing way to increase its function without the Action function code is necessary, for example, you might want to for a message to add more custom attributes, the method can capture information statistics, add additional database results, or control message flow velocity, etc.
In my example code below, the addition operation expects the left and right properties to be finite. In addition, it is useful to append the original input parameters to the output for debugging purposes. You can add validation check and debug information with the following code:
const seneca = require('seneca')()
seneca
.add(
'role:math,cmd:sum'.function(msg, respond) {
var sum = msg.left + msg.right
respond(null, {
answer: sum
})
})
// Rewrite role:math, CMD :sum with to add additional functionality
.add(
'role:math,cmd:sum'.function(msg, respond) {
// bail out early if there's a problem
if (!Number.isFinite(msg.left) ||
!Number.isFinite(msg.right)) {
return respond(new Error("Left and right values must be numeric."))}// Call the previous operation role:math, CMD :sum
this.prior({
role: 'math'.cmd: 'sum'.left: msg.left,
right: msg.right,
}, function(err, result) {
if (err) return respond(err)
result.info = msg.left + '+' + msg.right
respond(null, result)
})
})
// Added role:math, CMD :sum
.act('role: math, CMD: sum, left: 1.5, right: 2.5'.console.log // Print {answer: 4, info: '1.5+2.5'}
)
Copy the code
The Seneca instance provides a method called prior that allows you to invoke the old action function that it overrides in the current action method.
The prior function takes two arguments:
- MSG: message body
- Response_callback: callback function
In the example code above, you have shown how to modify the input and output parameters. It is optional to modify these parameters and values, for example, by adding new overwrites to add logging.
In the example above, we also demonstrate how to better handle errors. We verify that the data is correct before we actually do the operation. If the parameters passed in are incorrect, we simply return the error message instead of waiting for the system to report an error during the actual calculation.
Error messages should only be used to describe incorrect input or internal failure information. For example, if you perform some database query and no data is returned, this is not an error, but just a feedback of the fact of the database, but if the connection to the database fails, it is an error.
The above code can be found in the sum-valid.js file.
Use plug-ins to organize patterns
A Seneca instance is simply a collection of Action Patterms. You can use namespaces to organize the operation patterns. For example, in the previous example, we used roles: Math, Seneca also supports a minimalist plug-in support for logging and debugging.
Similarly, Seneca plug-in is a set of operation mode, it can have a name, is used to log entries, the annotation can also give plug-in is a set of options to control their behavior, plug-in also provides in the correct order mechanism to carry out the initialization function, for example, do you wish to attempt to establish a database connection before the data read from the database.
In simple terms, the Seneca plugin is just a function with a single parameter option. You pass the plugin definition function to the seneca.use method. Here is the smallest Seneca plugin (it does nothing!). :
function minimal_plugin(options) {
console.log(options)
}
require('seneca')()
.use(minimal_plugin, {foo: 'bar'})
Copy the code
The seneca.use method takes two parameters:
- Plugin: Plug-in defines a function or a plug-in name;
- Options: Plug-in configuration options
After the example code above is executed, the printed log looks like this:
{"kind":"notice"."notice":"Hello Seneca qk0ij5t2bta 3/1483584697034/62768/3.2.2 / -"."level":"info"."when":1483584697057}
(node:62768) DeprecationWarning: 'root' is deprecated, use 'global'
{ foo: 'bar' }
Copy the code
Seneca also provides detailed logging, which provides more log information for development or production. Generally, the log level is set to INFO, which does not print much log information. If you want to see all the log information, try starting your service like this:
node minimal-plugin.js --seneca.log.all
Copy the code
Will you be surprised? Of course, you can also filter log information:
node minimal-plugin.js --seneca.log.all | grep plugin:define
Copy the code
As you can see from the blog, Seneca has many built-in plugins, such as Basic, Transport, Web, and Mem-Store, which give us the basic functionality to create microservices. You should also see the Minimal_plugin.
Now, let’s add some modes of action to this plug-in:
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)
Copy the code
Running the math-plugin.js file yields the following information:
null { answer: 3 }
Copy the code
Look at a printed log:
{
"actid": "7ubgm65mcnfl/uatuklury90r"."msg": {
"role": "math"."cmd": "sum"."left": 1,
"right": 2."meta$": {
"id": "7ubgm65mcnfl/uatuklury90r"."tx": "uatuklury90r"."pattern": "cmd:sum,role:math"."action": "(bjx5u38uwyse)"."plugin_name": "math"."plugin_tag": "-"."prior": {
"chain": []."entry": true."depth": 0}."start": 1483587274794,
"sync": true
},
"plugin$": {
"name": "math"."tag": "-"
},
"tx$": "uatuklury90r"
},
"entry": true."prior": []."meta": {
"plugin_name": "math"."plugin_tag": "-"."plugin_fullname": "math"."raw": {
"role": "math"."cmd": "sum"
},
"sub": false."client": false."args": {
"role": "math"."cmd": "sum"
},
"rules": {},
"id": "(bjx5u38uwyse)"."pattern": "cmd:sum,role:math"."msgcanon": {
"cmd": "sum"."role": "math"
},
"priorpath": ""
},
"client": false."listen": false."transport": {},
"kind": "act"."case": "OUT"."duration": 35."result": {
"answer": 3}."level": "debug"."plugin_name": "math"."plugin_tag": "-"."pattern": "cmd:sum,role:math"."when": 1483587274829}Copy the code
All logs for the plugin are automatically added with plugin properties.
In Seneca’s world, we organize collections of modes of action through plug-ins, which make logging and debugging easier, and then you can combine multiple plug-ins into various microservices. In the next section, we’ll create a Math service.
Plug-in initialization through the need to do some work, such as connecting to the database, but, you don’t need in the definition of plug-in function to perform the initialization, define functions designed for synchronous implementation, because it is in all the operations defined a plug-in, in fact, you should not be defined in the function call Seneca. The act method, Just call the seneca.add method.
To initialize a plugin, you need to define a special matching pattern init:, which is called in sequence for each plugin. Init must call its callback function without any errors. If the plugin fails to initialize, Seneca will immediately exit the Node process. All plug-in initialization work must be completed before any operation can be performed.
To demonstrate initialization, let’s add a simple custom logging to the Math plug-in. When the plug-in starts, it opens a log file and writes a log of all operations to the file. The file needs to be successfully open and writable.
const fs = require('fs')
function math(options) {
// Log function, created by init function
var log
// Putting all the patterns together makes it easier for us to find them
this.add('role:math,cmd:sum', sum)
this.add('role:math,cmd:product', product)
// This is the special initialization operation
this.add('init:math', init)
function init(msg, respond) {
// Log to a feature file
fs.open(options.logfile, 'a'.function (err, fd) {
// If the file cannot be read or written, an error is returned, causing Seneca to fail to start
if (err) return respond(err)
log = makeLog(fd)
respond()
})
}
function sum(msg, respond) {
var out = { answer: msg.left + msg.right }
log('sum '+msg.left+'+'+msg.right+'='+out.answer+'\n')
respond(null, out)
}
function product(msg, respond) {
var out = { answer: msg.left * msg.right }
log('product '+msg.left+The '*'+msg.right+'='+out.answer+'\n')
respond(null, out)
}
function makeLog(fd) {
return function (entry) {
fs.write(fd, new Date().toISOString()+' '+entry, null.'utf8'.function (err) {
if (err) return console.log(err)
// Make sure the log entries are refreshed
fs.fsync(fd, function (err) {
if (err) return console.log(err)
})
})
}
}
}
require('seneca')()
.use(math, {logfile:'./math.log'})
.act('role:math,cmd:sum,left:1,right:2'.console.log)
Copy the code
In the code for the above plug-in, the matching patterns are organized at the top of the plug-in so that they are easier to see, the functions are defined a little below those patterns, and you can see how to use the option to provide the location of custom log files (self-evident, this is not a production log!). .
The init function performs some asynchronous file system work and therefore must be completed before any operations can be performed. If it fails, the entire service cannot be initialized. To see what happens when it fails, try changing the log file location to invalid, such as /math.log.
The above code can be found in the math-plugin-init.js file.
Creating microservices
Now let’s turn the Math plug-in into a true microservice. First, you need to organize your plug-ins. The business logic of the Math plug-in —-, which is what it provides, is separate from how it communicates with the outside world, you might expose a Web service, or you might listen on the message bus.
It makes sense to put the business logic (that is, the plug-in definition) in its own file. The node.js module can be implemented perfectly by creating a file named math.js that reads:
module.exports = function math(options) {
this.add('role:math,cmd:sum'.function sum(msg, respond) {
respond(null, { answer: msg.left + msg.right })
})
this.add('role:math,cmd:product'.function product(msg, respond) {
respond(null, { answer: msg.left * msg.right })
})
this.wrap('role:math'.function (msg, respond) {
msg.left = Number(msg.left).valueOf()
msg.right = Number(msg.right).valueOf()
this.prior(msg, respond)
})
}
Copy the code
We can then add it to our microservice system in the file that needs to reference it like this:
// Both of the following are equivalent (remember the two arguments to 'Seneca. Use'?).
require('seneca')()
.use(require('./math.js'))
.act('role:math,cmd:sum,left:1,right:2'.console.log)
require('seneca')()
.use('math') // Find './math.js' in the current directory
.act('role:math,cmd:sum,left:1,right:2'.console.log)
Copy the code
The Seneca. Wrap method matches a set of patterns, using the same action extension function to override all matched patterns. This has the same effect as manually calling Seneca.
- Pin: Indicates the pattern matching mode
- Action: The extended action function
Pin is a pattern that can match multiple patterns. For example, role: Math pin can match role:math, CMD :sum and role:math, CMD :product.
In the example above, in the wrap function at the end, we ensured that the left and right values in the body of any message passed to role:math were numbers, and that even if we passed strings, they would be automatically converted to numbers.
Sometimes it’s useful to see what actions have been overridden in the Seneca instance. You can start the application with — Seneca.print.tree.
require('seneca')()
.use('math')
Copy the code
Then execute it:
❯ node math-tree.js -- Seneca.print.tree {"kind":"notice"."notice":"Hello, Seneca abs0eg4hu04h / 1483589278500/65316/3.2.2 / -"."level":"info"."when":1483589278522}
(node:65316) DeprecationWarning: 'root' is deprecated, use 'global'
Seneca action patterns forInstance: abs0eg4hu04h / 1483589278500/65316/3.2.2 / - ├ ─ ┬ CMD: sum │ └ ─ ┬ role: math │ └ ─ ─# math, (15fqzd54pnsp),
│ # math, (qqrze3ub5vhl), sum├ ─┬ CMD :product ├ ─┬ role: Math ├ ─# math, (qnh86mgin4r6),
# math, (4nrxi5f6sp69), product
Copy the code
As you can see above, there are many key/value pairs and the overrides are shown in a tree structure. All Action functions are shown in the format #plugin, (action-id), function-name.
However, for now, all operations exist in the same process, so let’s create a file named math-service.js and fill in the following:
require('seneca')()
.use('math')
.listen()
Copy the code
We then start the script to start our microservice, which starts a process and listens for HTTP requests through port 10101. It is not a Web server, and HTTP serves only as a message transport mechanism at this point.
You can now visit http://localhost:10101/act? ro… To see the result, you can use the curl command:
curl -d '{"role":"math","cmd":"sum","left":1,"right":2}' http://localhost:10101/act
Copy the code
You can see the results either way:
{"answer": 3}Copy the code
Next, you need a microserver client math-client.js:
require('seneca')()
.client()
.act('role:math,cmd:sum,left:1,right:2'.console.log)
Copy the code
Open a new terminal and execute the script:
null { answer: 3 } { id: '7uuptvpf8iff/9wfb26kbqx55',
accept: '043 di4pxswq7/1483589685164/65429/3.2.2 / -,
track: undefined,
time:
{ client_sent: '0',
listen_recv: '0',
listen_sent: '0',
client_recv: 1483589898390 } }
Copy the code
In Seneca, we create the microservice via the seneca.Listen method and then communicate with the microservice via Seneca. Client. In the examples above, we are using Seneca’s default configuration, such as HTTP listening on port 10101, but both the Seneca. Listen and Seneca. Client methods can accept the following parameters in order to achieve this function:
- Port: an optional number, indicating the port number.
- Host: a string that can be used first, indicating the host name or IP address.
- Spec: Optional objects, complete custom objects
Note: On Windows, if host is not specified, the default connection will be 0.0.0.0, which is useless. You can set host to localhost instead.
Client and LISTEN can communicate as long as they have the same port number as the host:
- Seneca. Client (8080) – Seneca. Listen (8080).
- Seneca. Client (8080, ‘192.168.0.2’) → Seneca. Listen (8080, ‘192.168.0.2’)
- Seneca. Client ({port: 8080, host: ‘192.168.0.2’}) → Seneca. Listen ({port: 8080, host: ‘192.168.0.2’})
Seneca provides you with the dependency-free transport feature, so that you do not need to know how messages are transferred or which services will get them when you are developing business logic. Instead, you can specify in the service Settings code or configuration. For example, the code in the Math.js plug-in never needs to change, so we can change the transport mode at will.
While HTTP is convenient, it is not always appropriate. Another common protocol is TCP. We can easily use TCP for data transfer.
math-service-tcp.js :
require('seneca')()
.use('math')
.listen({type: 'tcp'})
Copy the code
math-client-tcp.js
require('seneca')()
.client({type: 'tcp'})
.act('role:math,cmd:sum,left:1,right:2'.console.log)
Copy the code
By default, the client/listen did not specify which message will be sent to where, only local defines the pattern, will be sent to the local mode, otherwise it will send to the server, all we can do some configuration to define what information will be sent to which services, you can use a pin parameters to do this.
Let’s create an application that will send all role: Math messages to the service over TCP and send all other messages locally:
Math – pin – service. Js:
require('seneca')()
.use('math')
// Listen for role:math messages
// Important: Must match client
.listen({ type: 'tcp'.pin: 'role:math' })
Copy the code
Math – pin – client. Js:
require('seneca') ()// Local mode
.add('say:hello'.function (msg, respond){ respond(null, {text: "Hi!"})})// Send role: Math mode to the service
// Note: Must match the server
.client({ type: 'tcp'.pin: 'role:math' })
// Remote operation
.act('role:math,cmd:sum,left:1,right:2'.console.log)
// Local operation
.act('say:hello'.console.log)
Copy the code
You can customize the print of logs through various filters to track the flow of messages, using — Seneca… Parameter, which can be configured as follows:
- Date-time: When the log entry was created;
- Seneca-id: Seneca process ID;
- Level: DEBUG, INFO, WARN, ERROR, or FATAL;
- Type: item code, such as act, plugin, etc.
- Plugin: plugin name, operations that are not within the plugin will be represented as root$;
- Case: Event of item: IN, ADD, OUT, etc
- Action-id /transaction-id: trace identifier,Always be consistent in the network;
- Pin: Action Matching mode;
- Message: Incoming/outgoing parameter message body
If you run the above process using –seneca.log.all, all logs will be printed. If you only want to see the logs printed by the Math plugin, you can start the service like this:
node math-pin-service.js --seneca.log=plugin:math
Copy the code
Web Services integration
Seneca is not a Web framework. However, you still need to connect it to your Web service apis, you always have to remember is that you do not keep exposed internal behavior pattern, this is not a good security practice, on the contrary, you should define a set of API patterns, such as with properties role: API, then you can connect them to your internal micro service.
Here’s how we define the api.js plug-in.
module.exports = function api(options) {
var validOps = { sum:'sum'.product:'product' }
this.add('role:api,path:calculate'.function (msg, respond) {
var operation = msg.args.params.operation
var left = msg.args.query.left
var right = msg.args.query.right
this.act('role:math', {
cmd: validOps[operation],
left: left,
right: right,
}, respond)
})
this.add('init:api'.function (msg, respond) {
this.act('role:web', {routes: {prefix: '/api'.pin: 'role:api,path:*'.map: {
calculate: { GET:true.suffix:'/{operation}' }
}
}}, respond)
})
}
Copy the code
Then, we use HApi as a Web framework to build the hapi-app.js application:
const Hapi = require('hapi');
const Seneca = require('seneca');
const SenecaWeb = require('seneca-web');
const config = {
adapter: require('seneca-web-adapter-hapi'),
context: (() = > {
const server = new Hapi.Server();
server.connection({
port: 3000
});
server.route({
path: '/routes'.method: 'get'.handler: (request, reply) = > {
const routes = server.table()[0].table.map(route= > {
return {
path: route.path,
method: route.method.toUpperCase(),
description: route.settings.description,
tags: route.settings.tags,
vhost: route.settings.vhost,
cors: route.settings.cors,
jsonp: route.settings.jsonp,
}
})
reply(routes)
}
});
returnserver; ()}});const seneca = Seneca()
.use(SenecaWeb, config)
.use('math')
.use('api')
.ready((a)= > {
const server = seneca.export('web/context') (); server.start((a)= > {
server.log('server started on: ' + server.info.uri);
});
});
Copy the code
After startup hapi – app. Js, visit http://localhost:3000/routes, you can see the following information:
[{"path": "/routes"."method": "GET"."cors": false
},
{
"path": "/api/calculate/{operation}"."method": "GET"."cors": false}]Copy the code
This means that we have successfully updated the pattern matching to the haPI applied route. Visit http://localhost:3000/api/cal… , will get the result:
{"answer": 3}Copy the code
In the example above, we directly loaded the Math plugin into the Seneca instance. We could have done this more reasonably, as shown in the hapi-app-client.js file:
. const seneca = Seneca() .use(SenecaWeb, config) .use('api')
.client({type: 'tcp'.pin: 'role:math'})
.ready((a)= > {
const server = seneca.export('web/context') (); server.start((a)= > {
server.log('server started on: ' + server.info.uri);
});
});
Copy the code
Instead of registering the Math plugin, we use the client method to send role: Math to the math-Pin-service.js service over a TCP connection, and yes, your microservice is formed.
Note: Never use the body of an external input to create an operation, always create it internally, which effectively avoids injection attacks.
In the initialization function above, a pattern operation for role:web is called and a routes attribute is defined. This defines a URL that matches the pattern operation with the following parameters:
- Prefix: indicates the URL prefix
- Pin: The set of patterns to be mapped
- Map: A list of PIN wildcard attributes to be used as URL Endpoint
Your URL will start at/API /.
The PIN for rol: API,path: * maps any pattern that has role=” API “key-value pairs and the path attribute is defined. In this case, only Role: API, Path :calculate fits the pattern.
The map attribute is an object that has a calculate attribute that corresponds to a URL address that starts with: / API /calculate.
By definition, the calculate value is an object that indicates that HTTP GET methods are allowed and that urls should have parameterized suffixes (suffixes are the same as in hapi’s route rules).
So, your full address is/API /calculate/{operation}.
The rest of the message properties are then retrieved either from the URL Query object or from the JSON Body, which in this case is absent because of the GET method.
SenecaWeb will describe a request via MSG. Args, which includes:
- Body: Payload part of the HTTP request;
- Query: QueryString for the request;
- Params: The path parameter of the request.
Now, launch the microservice we created earlier:
Node math-pin-service.js — Seneca. Log =plugin:math
Node hapi-app.js — Seneca. Log =plugin:web,plugin: API
http://localhost:3000/api/cal… 得到 {“answer”:6}
http://localhost:3000/api/cal… Get {” answer: “5}
PM2: Node service deployment (service cluster), management, and monitoring
Start the
pm2 start app.js
Copy the code
- -w –watch: listens for directory changes. If the directory changes, the application restarts automatically
- –ignore-file: Files ignored when listening for directory changes. Pm2 start rpc_server.js –watch –ignore-watch=”rpc_client.js”
- -n –name: specifies the name of an application, which can be used to distinguish applications
- -i –instances: sets the number of application instances. 0 is the same as Max
- -f –force: forcibly starts an application, usually when the same application is running
- -o –output: indicates the path of the standard output log file
- -e –error: indicates the path to the error log file
- –env: configures environment variables
For example, pm2 start rpc_server.js -w -i Max -n s1 –ignore-watch=”rpc_client.js” -e./server_error.log -o./server_info.log
In cluster-mode, i.e., -i Max, the log file is automatically appented with -${index} to ensure that the log file does not duplicate
Other simple and common commands
- pm2 stop app_name|app_id
- pm2 restart app_name|app_id
- pm2 delete app_name|app_id
- pm2 show app_name|app_id OR pm2 describe app_name|app_id
- pm2 list
- pm2 monit
- pm2 logs app_name|app_id –lines –err
Graceful Stop
pm2 stop app_name|app_id
Copy the code
process.on('SIGINT', () => {
logger.warn('SIGINT')
connection && connection.close()
process.exit(0)})Copy the code
When the process ends, the program intercepts the SIGINT signal and then executes process.exit() to gracefully exit the process after disconnecting the database and other memory-consuming operations before the process is killed. (If the process does not end after 1.6s, continue to send SIGKILL signal to force the process to end)
Process File
ecosystem.config.js
const appCfg = {
args: ' '.max_memory_restart: '150M'.env: {
NODE_ENV: 'development'
},
env_production: {
NODE_ENV: 'production'
},
// source map
source_map_support: true.// Do not merge log output for cluster service
merge_logs: false.// This parameter is used to limit the timeout period when an exception occurs during application startup
listen_timeout: 5000.// Process SIGINT command time limit, that is, the process must listen to SIGINT signal must be set at the following time to end the process
kill_timeout: 2000.// If the restart is not attempted after the startup is abnormal, the operation and maintenance personnel try to find the cause and retry
autorestart: false.// It is not allowed to start the process with the same script
force: false.// The command queue to be executed after the pull/upgrade operation is performed in Keymetrics Dashboard
post_update: ['npm install'].// Listen for file changes
watch: false.// Ignore listening for file changes
ignore_watch: ['node_modules']}function GeneratePM2AppConfig({ name = ' ', script = ' ', error_file = ' ', out_file = ' ', exec_mode = 'fork', instances = 1, args = "" }) {
if (name) {
return Object.assign({
name,
script: script || `${name}.js`.error_file: error_file || `${name}-err.log`.out_file: out_file|| `${name}-out.log`,
instances,
exec_mode: instances > 1 ? 'cluster' : 'fork',
args
}, appCfg)
} else {
return null}}module.exports = {
apps: [
GeneratePM2AppConfig({
name: 'client'.script: './rpc_client.js'
}),
GeneratePM2AppConfig({
name: 'server'.script: './rpc_server.js'.instances: 1}})]Copy the code
pm2 start ecosystem.config.js
Copy the code
The processFile is recommended to be named in *.config.js format. Otherwise there will be consequences.
summary
In this chapter, you learn the basics of Seneca and PM2, and you can build a microservice-oriented system.
reference
- senecajs:senecajs.org
- Node.js Microservices by David Gonzalez
- Senecajs Quick Start documentation: senecajs.org/getting-sta…
- Seneca: NodeJS micro service framework start guide: segmentfault.com/a/119000000…