Sirix.io是如何基於Vert.x和Kotlin協程構建非同步RESTful API

banq發表於2018-12-30

Sirix是一個儲存系統,它的核心是日誌結構,讀取可以是隨機的,並且在事務提交期間將寫入批處理並同步到磁碟。資料永遠不會寫回到同一個地方,因此不會就地修改,相反,Sirix在記錄級別使用寫時複製(COW)(因此,它建立頁面片段並且通常不復制整個頁面),每次必須修改頁面時,已更改的記錄都會寫入新位置,確切複製哪些記錄取決於所使用的版本控制演算法。

對資料庫/資源​​的更改發生在資源繫結事務中。因此,必須開啟ResourceManager才能建立寫入事務。在任何時候,只允許與N讀取事務同時進行一次寫入事務。每個事務都繫結到一個修訂版,而它們可以在任何修訂版上開啟,無論哪個修訂版都無關緊要。

Vert.x在Node.js和JVM之後進行了嚴格的建模。Vert.x中的所有內容都應該是非阻塞的。因此,稱為事件迴圈的單個執行緒可以處理大量請求。阻止呼叫必須在特殊的執行緒池上處理。預設值是每個CPU兩個事件迴圈(多反應器模式)。

我們正在使用Kotlin,因為它簡單而簡潔。其中一個非常有趣的功能是協同程式。從概念上講,它們就像非常輕量級的執行緒 另一方面,建立執行緒非常昂貴。關於協同程式的一個很酷的事情是,它們允許編寫幾乎像順序的非同步程式碼。每當一個協程將被掛起時,底層執行緒不會被阻塞並且可以被重用。在引擎蓋下,每個掛起函式透過Kotlin編譯器獲得另一個引數,這是一個延續,它儲存恢復函式的位置(正常恢復,恢復異常)。

Keycloak用作OAuth2授權伺服器(密碼憑據流量),因為我們決定不自己實現授權。

為了獲得訪問令牌,首先必須針對POST / login進行請求- 使用身份中作為JSON物件傳送的使用者名稱/密碼憑證進行路由。實現程式碼:

post("/login").produces("application/json").coroutineHandler { rc ->
    val userJson = rc.bodyAsJson
    val user = keycloak.authenticateAwait(userJson)
    rc.response().end(user.principal().toString())
}


coroutine-handler是一個簡單的擴充套件函式:

/* An extension method for simplifying coroutines usage with Vert.x Web routers. */
private fun Route.coroutineHandler(fn: suspend (RoutingContext) -> Unit) {
  handler { ctx ->
    launch(ctx.vertx().dispatcher()) {
      try {
        fn(ctx)
      } catch (e: Exception) {
        ctx.fail(e)
      }
    }
  }
}


協程式在Vert.x事件迴圈(排程程式)上啟動。
這是為了執行更長的執行處理程式:

vertxContext.executeBlockingAwait(Handler < Future < Nothing >> {
  //更長時間執行任務
})


Vert.x為這類任務使用不同的執行緒池。因此,該任務在另一個執行緒中執行。請注意當協程被暫停,事件迴圈不會被阻止。

現在我們再次將焦點轉移到我們的API,並展示它是如何設計的。我們首先需要設定我們的伺服器和Keycloak。

一旦兩個伺服器都啟動並執行,我們就能夠編寫一個簡單的HTTP客戶端。我們首先必須讓/login使用指定的“使用者名稱/密碼”JSON-Object 從端點獲取令牌。在Kotlin中使用非同步HTTP客戶端(來自Vert.x),它看起來像這樣:

val server = "https://localhost:9443"

val credentials = json {
  obj("username" to "testUser",
      "password" to "testPass")
}

val response = client.postAbs("$server/login").sendJsonAwait(credentials)

if (200 == response.statusCode()) {
  val user = response.bodyAsJsonObject()
  val accessToken = user.getString("access_token")
}


然後,必須在Authorization HTTP-Header中為每個後續請求傳送此訪問令牌。儲存第一個資源看起來像這樣(簡單的HTTP PUT-Request):

val xml = """
    <xml>
      foo
      <bar/>
    </xml>
""".trimIndent()

var httpResponse = client.putAbs("$server/database/resource1").putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer $accessToken").sendBufferAwait(Buffer.buffer(xml))

if (200 == response.statusCode()) {
  println("Stored document.")
} else {
  println("Something went wrong ${response.message}")
}


首先,建立一個名稱database帶有一些後設資料的空資料庫,然後使用名稱儲存XML片段resource1。PUT HTTP-Request是冪等的。具有相同URL端點的另一個PUT-Request將刪除以前的資料庫和資源並重新建立資料庫/資源​

HTTP-Response應為200,產生HTTP-body:

<rest:sequence xmlns:rest="https://sirix.io/rest">
  <rest:item>
    <xml rest:id="1">
      foo
      <bar rest:id="3"/>
    </xml>
  </rest:item>
</rest:sequence>


以上是從儲存系統為元素節點序列化生成ID。

然後透過GET HTTP-Request,https://localhost:9443/database/resource1我們還可以再次檢索儲存的資源。
然而,到目前為止,這並不是很有趣。我們可以更新資源POST-Request。假設我們像以前一樣檢索了訪問令牌,我們可以簡單地執行POST-Request並使用我們之前收集的有關node-ID的資訊:

val xml = """
    <test>
      yikes
      <bar/>
    </test>
""".trimIndent()

val url = "$server/database/resource1?nodeId=3&insert=asFirstChild"

val httpResponse = client.postAbs(url).putHeader(HttpHeaders.AUTHORIZATION
                         .toString(), "Bearer $accessToken").sendBufferAwait(Buffer.buffer(xml))


有趣的部分是URL,我們用作端點。我們簡單地說,選擇ID為3的節點,然後將給定的XML片段作為第一個子片段插入。這將生成以下序列化XML文件:

<rest:sequence xmlns:rest="https://sirix.io/rest">
  <rest:item>
    <xml rest:id="1">
      foo
      <bar rest:id="3">
        <test rest:id="4">
          yikes
          <bar rest:id="6"/>
        </test>
      </bar>
    </xml>
  </rest:item>
</rest:sequence>


每個PUT-以及POST請求都隱含commits了底層事務。因此,我們現在能夠再次傳送第一個GET請求來檢索整個資源的內容,例如透過指定一個簡單的XPath查詢,在所有版本中選擇根節點GET https://localhost:9443/database/resource1?query=/xml/all-time::*並獲得以下XPath結果:

<rest:sequence xmlns:rest="https://sirix.io/rest">
  <rest:item rest:revision="1" rest:revisionTimestamp="2018-12-20T18:44:39.464Z">
    <xml rest:id="1">
      foo
      <bar rest:id="3"/>
    </xml>
  </rest:item>
  <rest:item rest:revision="2" rest:revisionTimestamp="2018-12-20T18:44:39.518Z">
    <xml rest:id="1">
      foo
      <bar rest:id="3">
        <xml rest:id="4">
          foo
          <bar rest:id="6"/>
        </xml>
      </bar>
    </xml>
  </rest:item>
</rest:sequence>


一般來說,我們支援幾個額外的時間XPath軸:future ::,future-or-self ::,past ::,past-or-self ::,previous ::,previous-or-self ::,next ::, next-or-self ::,first ::,last ::,all-time ::

透過在GET請求中指定序列化(開始和結束脩訂引數)的一系列修訂,可以實現相同的目的:
GET https://localhost:9443/database/resource1?start-revision=1&end-revision=2
或透過時間戳:
GET https://localhost:9443/database/resource1?start-revision-timestamp=2018-12-20T18:00:00&end-revision-timestamp=2018-12-20T19:00:00
我們肯定也能夠透過更新XQuery表示式(不是非常RESTful)或使用簡單的DELETEHTTP請求來刪除資源或其任何子樹:

val url = "$server/database/resource1?nodeId=3"

val httpResponse = client.deleteAbs(url).putHeader(HttpHeaders.AUTHORIZATION
                         .toString(), "Bearer $accessToken").sendAwait()

if (200 == httpResponse.statusCode()) {
  ...
}


這將刪除ID為3的節點,在我們的例子中,因為它是整個子樹的元素節點。肯定它已作為修訂版3提交,因此所有舊版本仍然可以查詢整個子樹(或者在第一個修訂版中,它只是名稱為“bar”而沒有任何子樹的元素)。
如果我們想得到一個差異,目前以XQuery Update語句的形式(但我們可以以任何格式序列化它們),只需呼叫XQuery函式sdb:diff,該函式定義為:
sdb:diff($coll as xs:string, $res as xs:string, $rev1 as xs:int, $rev2 as xs:int) as xs:string

例如,透過我們上面建立的資料庫/ resource1這樣的GET請求,我們可以發出以下請求:
GET https://localhost:9443/?query=sdb%3Adiff%28%27database%27%2C%27resource1%27%2C1%2C2%29
請注意,query-String必須進行URL編碼,然後對其進行解碼

sdb:diff('database','resource1',1,2)
在我們的示例中,diff的輸出是包含在封閉sequence-element中的XQuery-Update語句:

<rest:sequence xmlns:rest="https://sirix.io/rest">
  let $doc := sdb:doc('database','resource1', 1)
  return (
    insert nodes <xml>foo<bar/></xml> as first into sdb:select-node($doc, 3)
  )
</rest:sequence>


這意味著resource1從database第一次修訂中開啟,然後將子樹<xml>foo<bar/></xml>附加到具有穩定節點ID 3作為第一子節點的節點。

相關文章