Implementing a REST API for Microservices with Clojure using Pedestal

Machine Learning Technology Artificial Intelligence Technology Natural Language Processing Technology   Semantic Web Technology Search Technology DataBase Technology Ontology Technology Digital Transformation Technology User Interface and DataVisualization Workflow & Services Navigation of this blog Algorithm Clojure Microservice

Pedestal Concept

From Microservices with Clojure. In the previous article, we described an overview of REST APIs for microservices. In this article, we will describe a concrete implementation of REST API for microservices by Clojure using Pedestal.

Pedestal is an API-first Clojure framework, a data-driven extensible framework that provides a set of libraries for building reliable concurrent services with dynamic properties, implemented using protocols to reduce coupling between components It is implemented using protocols to reduce coupling between components. It prioritizes data over functions and functions over macros. Data-driven routes and handlers can be created, allowing them to behave differently at runtime depending on incoming requests. This allows the creation of flexible and dynamic services suitable for microservice-based applications. It also supports building scalable asynchronous services using server-sent events (SSE) and WebSockets.

The Pedestal architecture consists of two main concepts, Interceptors and Context Map, and two sub-concepts, Chain Provider and Network Connectors. Core of the Pedestal framework All logic is implemented as Interceptors, but the HTTP connection handler is separated, creating an initial context map to initiate execution and a Chain Provider interface to set up the Interceptor queue. Pedestal includes a Servlet Chain Provider, which works with any HTTP server that handles servlets. Pedestal applications are not limited to HTTP. Custom chain providers can be written to support other application protocols. The combination of target chain providers and network connectors also makes it possible to work with different transport protocols, such as UDP, which is highly reliable.

Pedestal initially had two independent parts: the Pedestal application and the Pedestal server; the Pedestal application was a ClojureScript-based front-end framework that is now obsolete. Today, the focus is solely on the Pedestal server in order to build reliable services and APIs.

Interceptors

Pedestal’s Interceptor is based on a software design pattern called Interceptor. An interceptor is a service extension that registers events of interest to the framework and is called by the framework when those events occur in the control flow. When an interceptor is called, it performs its function and the control flow returns to the framework.

Most of Pedestal’s core logic consists of one or more interceptors, which can be combined to build a chain of interceptors. pedestal interceptors are defined in Clojure’s Map, as shown in the following figure The :name key contains the namespace keyword of the interceptor and is optional. The :enter and :leave keys specify a Clojure function that takes a Context Map as input and returns a Context Map as output. To register an interceptor with the Pedestal framework, either the :enter or :leave key must be defined. The function specified by the :enter key is called by the Pedestal framework when data flows into the interceptor. The function specified by :leave is called when a response is returned from the interceptor.

The function specified by :error is called when the execution of the interceptor raises an exception event or when the execution fails with an error. To handle exception events, the :error key specifies a Clojure function that takes two arguments: the Context Map and the ex-info exception thrown by the interceptor. This returns a Context Map with the exception reconnected so that other interceptors can handle it, or throws the exception so that the Pedestal framework can handle it.

interceptor chain

Pedestal Interceptors can be organized as a chain of interceptors according to the Chain of Responsibility design pattern. Each interceptor performs exactly one task, and together as a chain of interceptors, they can accomplish a larger task consisting of one or more tasks.

The Chain of Responsibility pattern helps navigate the complex structure of an interceptor chain. The flow of control within the interceptor chain is controlled by the Context Map, which itself is passed as input to each interceptor, allowing the interceptor to arbitrarily add, remove, or reorder interceptors in the chain. interceptors in the chain at will. This is the reason why most of the Web framework modules, such as routing, content negotiation, and request handlers, are implemented as Pedestal interceptors.

The :enter and :leave interceptor functions must return a Context Map as value for the execution flow to proceed to the next interceptor. If nil is returned, the Pedestal framework reports an internal server error and the execution flow terminates. The interceptor may return a Core.async channel. In that case, the channel is treated as a promise that a context map will be delivered in the future. When the channel sends out the Context Map, the chain executor closes the channel.

The chain executor calls each interceptor in the go block of the core.async library, so even if one interceptor is called in a different thread than the next, all bindings will be communicated to each interceptor. In scenarios where interceptors take a long time to process requests or call external APIs, it is recommended to use the go block to send the channel as a return value and let Pedestal continue executing asynchronously. when Pedestal receives the channel as an output, it will call Pedestal receives the channel as output, drops the interceptor thread, and waits for a value to be generated from the channel. Only one value can be consumed from the channel, and it must be a context map.

As shown in the previous figure, while running an interceptor chain, all :enter functions are called in the order of the interceptors listed in the chain. When the :enter functions of all interceptors are called, the resulting Context Map is called in reverse order through the :leave functions.

Since any interceptor in the chain can be returned asynchronously, Pedestal creates what is called a virtual call stack of interceptors. This holds a queue of interceptors whose :enter function should be called, and a stack of interceptors whose :enter function has been called but whose :leave function is pending. By keeping a stack, the Pedestal can call the :leave function in the reverse order of the :enter function calls of the interceptors on the chain. Both these queues and stacks are kept in the Context Map and are accessible by the Interceptor.

Since Interceptors have access to the Context Map, they can change the execution plan for the remaining requests by changing the order of Interceptors in the chain. They can enqueue additional interceptors as well as skip all remaining interceptors in the chain and terminate the request.

Importance of Context Map

Context is merely a Clojure map that the interceptor receives as input and produces as output. It contains all the values that control the Pedestal application, including interceptors, chains, execution stacks, queues, and context values that may be generated by the interceptor or needed by interceptors remaining in the interceptor chain. The context map also contains a set of predicate functions. If any one of the predicate functions returns true, the chain terminates. To enable asynchronous functionality and facilitate interaction between the platform and interceptor, all necessary functions and signal keys are also defined in the Context Map. The table shown here will be a partial list of keys that the interceptor chain will maintain in the Context Map.

If the :bindings map is modified by an interceptor and returned in the output context map, Pedestal installs the new bindings as thread-local bindings before executing the next interceptor in the chain. io. The pedestal.interceptor.chain/queue context key contains all interceptors that have not yet been executed. The first interceptor in the queue is the interceptor that is considered to be executed next by calling the :enter function. This key must be used for debugging purposes only. To change the queue or execution flow, instead of changing the value of this key, the enqueue, terminate, and terminate-when of the interceptor chain must be called. io.pedestal.interceptor.chain/ The termination predicate specified by the key of terminators shall be checked for a true predicate each time the :enter function is called. If a valid predicate is found, Pedestal skips the :enter function of all remaining interceptors and executes the :leave function of the interceptors in the stack to begin terminating the execution flow.

The io.pedestal.interceptor.chain/stack context key contains the Interceptor whose :enter function has already been called but whose :leave function is pending. The Interceptor at the top of the stack is executed first, so that the :leave function is called in the reverse order of the :enter function call.

In addition to keys added by the interceptor chain, the context map may also contain keys added by other interceptors. For example, the standard servlet interceptors provided by Pedestal add servlet-specific keys to the context map, such as :servlet-request, :servlet- response, :servlet-config, and :servlet. When dealing with HTTP servers, the Context Map also has the keys :request and :response, which are assigned the maps request and response, respectively.

Pedestal is not limited to HTTP services, but can extend various services: it can extend services to systems such as Kafka, or use different protocols such as SCTP, Reliable UDP, or UDT. pedestal.service The Pedestal module will be a collection of interceptors specialized for HTTP.

Implementing the Pedestal Service

Pedestal provides a leiningen template named pedestal-service to create a new project with the necessary dependencies and directory layout for the Pedestal service. To create a new project using this template, use the lein command with the template name and project name as follows

lein new pedestal-service pedestal-play

The lein command creates a new directory with the specified project name and adds all necessary dependencies to the project.clj file. The directory tree of the created project will look like this

> tree pedestal-play
pedestal-play
├── Capstanfile
├── config
│ └── logback.xml ├── Dockerfile
├── project.clj
├── README.md
├── src
│  |_____pedestal_play
│          |_______ server.clj
│          |_______ service.clj
└── test
   └── pedestal_play 
        └── service_test.clj

To run the project, simply use the lein run command, and the sample service defined in the template will be compiled and started on port 8080. To test the service, open http://localhost:8080 and http://localhost:8080/about in a browser and observe the responses. The first URL will return Hello World! and the second URL will return Clojure 1.10.0 – served from /about as the response.

Both endpoints can also be accessed via cURL as follows.

>curl -v http://localhost:8080

*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Tue, 30 Aug 2022 00:59:05 GMT
< Strict-Transport-Security: max-age=31536000; includeSubdomains
< X-Frame-Options: DENY
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< X-Download-Options: noopen
< X-Permitted-Cross-Domain-Policies: none
< Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
< Content-Type: text/html;charset=utf-8
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
Hello World!%
Using interceptors and handlers

In the pedestal-play project, the source file service.clj defines two interceptors, about-page and home-page, used in the service. These are used for the service. In addition, the HTTP-specific interceptors body-params and html-body are used, which are ready to be prepared by the pedestal-service module. A snippet of the pedestal-play.service namespace is provided in the shown here with the interceptor declaration.

(ns pedestal-play.service
  (:require [io.pedestal.http :as http]
            [io.pedestal.http.route :as route]
            [io.pedestal.http.body-params :as body-params]
            [ring.util.response :as ring-resp]))

(defn about-page
  [request]
  (ring-resp/response (format "Clojure %s - served from %s"
                              (clojure-version)
                              (route/url-for ::about-page))))

(defn home-page
  [request]
  (ring-resp/response "Hello World!"))

Instead of taking a Context Map as input and returning a Context Map, the about-page and home-page interceptors take a Request Map as argument and return a Response Map. Such interceptors are called handlers and are treated as functions in the Pedestal framework; handlers in the Pedestal framework do not have access to the Context Map. Therefore, it is not possible to change the order of interceptors in the chain.

The response function used by the home-page and about-page handlers is a utility function provided by the Ring framework and is used to create a Ring response map with status 200, no headers, and body content. body- params function parses the request body based on its MIME type and returns an interceptor that adds the associated key with the corresponding body parameters to the request map. For example, this function adds a :form-params key containing all form parameters to a request with content type application/x-www-form-urlencoded.
The interceptor specified by html- body var adds a Content-Type header parameter of type text/html;charset=UTF-8 to the response. Thus, since html-body operates on the response, the functions of this interceptor are defined with the :leave key. The functions of body-params, on the other hand, are defined by the :enter key of the interceptor map.

All Pedestal interceptors implement the IntoInterceptor protocol; Pedestal extends the IntoInterceptor protocol to Map, Function, List, Symbol, and Var, and allows the definition of Interceptors in these extended forms. Interceptor can be defined. The most common way to define an interceptor is as a Clojure Map with the keys :enter, :leave, and :error. They can also be defined as handlers.

Create routes

Pedestal interceptors are attached to routes that define endpoints for clients to interact with the application. pedestal provides three prominent ways to define routes: verbose, table, and terse. all three are defined as Clojure data structures, with verbose defined as a map, table as a set, and terse as a vector. The three formats are defined for convenience; Pedestal internally converts all route definitions to verbose format before processing them using the convenience function expand-routes. The verbose syntax is a list of maps with keywords defining the routes. The list of keywords and their descriptions are shown in the following table.

In addition, keys derived from the :path parameter of the verbose syntax include the following

The pedestal.route Pedestal module contains implementations of both Route and Router. router is a special interceptor that takes Routes as input in a verbose form and processes requests coming into Routes based on the implementation. The Router ensures that requests are processed through Interceptors according to the Interceptor Chain. Changes in the interceptor chain by the Interceptor are handled efficiently by the Router; in the pedestal-play project, two Routes, GET / and GET /about, are defined in the service.clj source file, with the following They are bundled in an interceptor chain with home-page and about-page handlers at the end, respectively, as shown in the code below.

(def routes #{["/" :get (conj common-interceptors `home-page)]
              ["/about" :get (conj common-interceptors `about-page)]})

The :app-name, :host, :port, and :scheme keys may also be specified as maps that apply to all routes specified in the route list, as shown in the following code.

(def routes #{{:app-name "Pedestal Play" :host "localhost" :port 8080
   :scheme :http}
                 ["/" :get (conj common-interceptors `home-page)]
                 ["/about" :get (conj common-interceptors `about-page)]})
Declare routers

The routers will be functions added to the chain as interceptors to analyze requests based on the defined Routes. pedestal, when creating a router, creates the router based on the values of the :io.pedestal.http/routes and : io.pedestal.http/router based on the values of the :io.pedestal.http/router keys. The service map contains all the details for Pedestal to create the service, including router, route, and chain provider properties. It serves as the builder of the Pedestal service.
Pedestal has three built-in routers, which can be specified using :map-tree, :prefix-tree, and :prefix-tree. The :map-tree is the default router and has a certain time complexity when applied to all static routes. If a route has a path parameter or wildcard, it falls back to prefix-tree.
If the value of io.pedestal.http/router is specified as a function, the router is constructed using that function. The function must take one argument, a route collection in verbose form, and return a router that satisfies the router protocol.

Access request parameters

The Servlet Chain Provider attaches the request map to the context map. The request map contains all form, query, and URL parameters specified by the API’s client. These parameters are defined as a map of key/value pairs, where each key represents a parameter specified by the client. All parameters are optional and exist only if specified by the client in the request. The following is a list of keys that may be included in the request map.

Apart from the parameters, there are also these keys that are always present in the request map and can be used by interceptors in the chain.

To check the request map, add a new debug-page handler to the pedestal-play project and map it to the root /debug, as shown in the following code. This handler debug-page will return a request map in response that contains only the parameter keys of interest. The code also uses the Cheshire library to convert it to a JSON string.

(defn debug-page
     [request]
     (ring-resp/response
      (cheshire.core/generate-string
       (select-keys request
          [:params :path-params :query-params :form-params]))))
   ;; Common Interceptors used for all routes
   (def common-interceptors [(body-params/body-params) http/html-body])
   (def routes #{{:app-name "Pedestal Play" :host "localhost" :port 8080
   :scheme :http}
                 ["/" :get (conj common-interceptors `home-page)]
                 ["/about" :get (conj common-interceptors `about-page)]
                 ["/debug/:id" :post (conj common-interceptors `debug-page)]})

As shown in the following example, a cURL request to the /debug root provides an entire request map that can be examined for path, query, and form parameters.

>curl -XPOST -d "formparam=1" "http://localhost:8080/debug/1?qparam=1"
{
  "params": {
    "qparam": "1",
    "formparam": "1"
  },
  "path-params": {
  "id": "1" },
  "query-params": {
    "qparam": "1"
  },
  "form-params": {
    "formparam": "1"
  }
}
Create interceptors

Pedestal interceptors can be defined as maps keyed by :name, :enter, :leave, and :error. For example, one could define an interceptor called msg-play and modify it to capitalize the query parameter name for the /hello root of the pedestal-play project and add a greeting upon exit using the :leave function.

;; Handler for /hello route
   (defn hello-page
     [request]
     (ring-resp/response
      (let [resp (clojure.string/trim (get-in request [:query-params :name]))]
        (if (empty? resp) "Hello World!" (str "Hello " resp "!")))))
   (def msg-play
     {:name ::msg-play
      :enter
      (fn [context]
         (update-in context [:request :query-params :name]
   clojure.string/upper-case))
:leave
  (fn [context] (update-in context [:response :body]
                               #(str % "Good to see you!")))})
   ;; Common Interceptors used for all routes
   (def common-interceptors [(body-params/body-params) http/html-body])
   (def routes #{{:app-name "Pedestal Play" :host "localhost" :port 8080
   :scheme :http}
                 ["/" :get (conj common-interceptors `home-page)]
                 ["/about" :get (conj common-interceptors `about-page)]
                 ["/debug/:id" :post (conj common-interceptors `debug-page)]
                 ["/hello" :get
                  (conj common-interceptors `msg-play `hello-page)]}) 

Requesting the /hello route from cURL with the name query parameter will trigger msg-‘s :enter and :leave events and produce the expected results.

>curl "http://localhost:8080/hello?name=clojure"
 Hello CLOJURE!Good to see you!

If no query parameter is specified, it shall return a Hello World greeting according to the implementation of the hello-page interceptor.

>curl "http://localhost:8080/hello?name="
 Hello World!Good to see you!

In the next article, we will discuss database construction for microservices using Next Generation DB Datomic.

コメント

タイトルとURLをコピーしました