Skip to content

Microservices

Tim Hardcastle edited this page Feb 29, 2024 · 14 revisions

Services running on the same hub can talk to one another directly.

To demonstrate this, we need two scripts. Here is examples/bar.pf:

cmd

returnTheThing(s) :
    post "Hello " + s + "!" to Output()

doTheThing(s) : 
    post "**** Hello " + s + "! ****" to Terminal()

Let's start it up in the REPL:

→ hub run "examples/bar.pf" as "Bar"
Starting script 'examples/bar.pf' as service 'Bar'.
Bar → returnTheThing "Marmaduke"
Hello Marmaduke!
Bar → 

Now let's take a look at examples/foo.pf:

contacts

Bar::"examples/bar.pf"

cmd

getTheThing(s) :
    get x from Bar --- returnTheThing |s|
    post "* " + x + " *" to Output()

makeBarDoTheThing(s) : 
    post Bar --- doTheThing |s|

Note the word contacts. This is a headword, as defined earlier. What it does is declare that the service expects to be able to talk to another service called Bar.

So if we try it out in the REPL:

Bar → hub run "examples/foo.pf" as "Foo" 
Starting script 'examples/foo.pf' as service 'Foo'.
Foo → getTheThing "Marmaduke"                                                                        
* Hello Marmaduke! *
Foo → makeBarDoTheThing "Ermintrude"
"**** Hello Ermintrude! ****"
Foo →

Some things to note:

  • Just as with SQL, we use the | delimiter to indicate expressions to be embedded from the calling service. So we must write get x from Bar --- returnTheThing |s| and not just get x from Bar --- returnTheThing s because in the latter case the service Bar would look for the variable s in its own environment, fail to find it, and return an error. As with embedding in SQL, the |...| can contain any expression that makes sense in the calling service, not just variable names.

  • What the calling service gets with a get command is whatever the called service posts to Output.

  • If the called service posts nothing to Output (except ok, as explained below) then you should use post, put, or delete. As usual it is up to you to make sure that the name of the command reflects the semantics.

  • You are only allowed to call commands, not functions. This is because any two services which wanted to have a function in common could and should put it in a common library.

  • When it comes to calling commands, the calling service has exactly the same access rights to the called service as would a human user talking to the REPL of the called service. What is private from the REPL is private from another service, and vice-versa.

  • If hotcoding is turned on, then a contact of your service is rebuilt if its source code has been changed.

Output versus Terminal

In the page on Basic IO, we noted that you could output either to Terminal or to Output, and promised to explain the difference later. Now is the time.

Sending a value to Terminal prints a string representation of the value directly to the terminal that the hub is running on. Sending it to Output sends the value to whoever or whatever is making the request, whether it's you in the REPL, another Pipefish service, a desktop Pipefish client, or whatever.

At present the string representation sent to Terminal is in the form of a Pipefish literal, e.g. "Hello world!" is displayed as "Hello world!" and not Hello world! as it would appear in the REPL. This feature is under review and may not be stable.

One other difference is that when Pipefish is asked to do something it always sends something to Output. If it sends nothing else, it sends what is internally called a Successful Object but which you will have met as the little green ok that you get as a result when you ask Pipefish to do something and there is no other output.

So when talking to another service, a command that posts an ok to Output must be accessed by put, post, or delete, and a command that posts an actual value to Output() must be accessed by get. A command that conditionally does one or the other will break either way of accessing it. Don't do that.

Comparison of imports and microservices

A contact of a service is, by definition, another service running on the hub. Whereas an imported module of a service is inside of that service.

So a service communicates with its contacts by means of put and post and get commands, whereas it accesses its imported modules by namespacing, e.g. strings.toUpper("hello").

When Service A launches service B as a contact, this does not imply that service B will automatically be shut down when service A shuts down. This must be done explicitly. Imported modules, by contrast, are inside the service that imports them, and so are shut down and rebuilt along with the service.

Clone this wiki locally