2020年7月26日 星期日

解決 switchIfEmpty() 每次都被執行的問題

紀錄一下最近發現的有點意外的 Reactor 反應。

Mono.justOrEmpty(getXXX())
    .switchIfEmpty(fallbackXXX())
    ...

本來預期的行為是,getXXX() 會回覆一個 Optional 物件,當回覆的 Optionalempty 時,才會觸發 fallbackXXX() 去呼叫外部服務。但結果在測試時發現 fallbackXXX() 每次都必然會被執行,雖然最後結果會是對的,如果 getXXX() 有回覆時,走到後面的 stream 內容會是 getXXX() 回覆的東西,但這樣就等於每次執行時都會呼叫外部服務了,而且呼叫後拿到的結果還不一定會用到。

查了好一段時間之後,發現問題好像是出在 Java 本身的 method evaluation 行為上,然後解決方法是必須把 fallbackXXX()Mono.defer() 包裝起來,以達成讓 fallbackXXX() 推遲被執行的目的。

Mono.justOrEmpty(getXXX())
    .switchIfEmpty( Mono.defer(fallbackXXX()) )
    ...
參考資料
  1. Mono switchIfEmpty() is always called

2020年7月12日 星期日

Scala 的 HTTP 伺服器:http4s

在 Ubuntu 準備 sbt 環境

雖然覺得這個步驟蠻沒意義的,不過總之我又久違地在 Windows 上開了個 Ubuntu 20.04 的 container。

在安裝 sbt 之前,需要先加入 sbt 的 repository 到 apt 的來源。

echo "deb https://dl.bintray.com/sbt/debian /" | sudo tee -a /etc/apt/sources.list.d/sbt.list
curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x2EE0EA64E40A89B84B2DF73499E82A75642AC823" | sudo apt-key add
sudo apt-get update

接著就是安裝 Java 與 sbt。

sudo apt-get install -y default-jre sbt

以我目前這個時間點來說,會裝出來的版本如下:

Java: 11.0.7
sbt: 1.3.13
產生 http4s 範例專案

執行以下的初始化指令,讓 sbt 產生一個 http4s 的範例小專案。

sbt new http4s/http4s.g8 -b 0.21

這個指令其實本來在 http4s 的 Quick Start 裡是還有指定 sbt 版本的 -sbt-version 1.3.12,不過這裡就把它拿掉,讓它直接用剛裝的版本了。產生過程會問一些問題,這裡基本上我全都用預設值了。

接著就可以來看看產生出來的檔案有哪些內容了:

build.sbt

organization := "com.example"
name := "scala-test"
version := "0.0.1-SNAPSHOT"
scalaVersion := "2.12.4"

val Http4sVersion = "0.16.6a"
val Specs2Version = "4.0.2"
val LogbackVersion = "1.2.3"

libraryDependencies ++= Seq(
  "org.http4s"     %% "http4s-blaze-server"  % Http4sVersion,
  "org.http4s"     %% "http4s-circe"         % Http4sVersion,
  "org.http4s"     %% "http4s-dsl"           % Http4sVersion,
  "org.specs2"     %% "specs2-core"          % Specs2Version % "test",
  "ch.qos.logback" %  "logback-classic"      % LogbackVersion
)

HelloWorld.scala

package com.example.scalatest

import io.circe._
import org.http4s._
import org.http4s.circe._
import org.http4s.server._
import org.http4s.dsl._

object HelloWorld {
  val service = HttpService {
    case GET -> Root / "hello" / name =>
      Ok(Json.obj("message" -> Json.fromString(s"Hello, ${name}")))
  }
}

Server.scala

package com.example.scalatest

import scala.util.Properties.envOrNone
import org.http4s.server.blaze.BlazeBuilder
import org.http4s.util.ProcessApp
import scalaz.concurrent.Task
import scalaz.stream.Process

object Server extends ProcessApp {
  val port: Int = envOrNone("HTTP_PORT").fold(8080)(_.toInt)

  def process(args: List[String]): Process[Task, Nothing] = BlazeBuilder.bindHttp(port)
    .mountService(HelloWorld.service, "/")
    .serve
}

這樣就是個最簡單能跑的 Scala + http4s 的小專案了,可以透過 sbt run 啟動伺服器。執行後可以看到像是這樣的啟動訊息:

[info] Loading settings from plugins.sbt ...
[info] Loading project definition from G:\java\intellij\scala-http4s-test\project
[info] Loading settings from build.sbt ...
[info] Set current project to scala-test (in build file:/G:/java/intellij/scala-http4s-test/)
[info] Running com.example.scalatest.Server
[pool-6-thread-4] INFO  o.h.b.c.n.NIO1SocketServerGroup - Service bound to address /127.0.0.1:8080
[pool-6-thread-4] INFO  o.h.s.b.BlazeBuilder - http4s v0.16.6a on blaze v0.12.11 started at http://127.0.0.1:8080/

啟動完成後,就可以用瀏覽器連上 http://127.0.0.1:8080/ 了,例如範例的 REST API 網址會是 http://127.0.0.1:8080/hello/michael,因為輸入的 {name} 是 michael,所以就會獲得 {"message":"Hello, michael"} 的 JSON 回覆。

參考資料
  1. Installing sbt on Linux

2020年4月19日 星期日

Replications

本篇為 Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems [1] 的讀書筆記。

Replication 指的是在分散式系統中將一份資料複製並存放在多個節點上,以帶來幾個可能的好處:

  • 將資料複製到距離使用者較近的節點,以減少 latency
  • 提高 availability
  • 增加讀取的 throughput

一般討論分散式系統的 replication 時,關注的困難點都是如何處理資料的變化。因為若資料不會變化,那只要整個複製貼上就完工了,沒什麼困難點。但若資料會變化,那麼就會需要考慮多個節點間要如何控管資料的變化了。

2020年4月8日 星期三

attribute 的記憶體需求

Vespa 的欄位如果 index-type 被宣告為 attribute 的話,就會直接被放在記憶體上,因此實務上會需要估計到底需要多少記憶體才能撐住所有的資料的問題。

2020年3月29日 星期日

Reactive Programming(三):Project Reactor

本系列的文章為 Modern Java in Action / Project Reactor 的讀書筆記,因此內容可能會有點跳躍。

2020年3月18日 星期三

在 Spock Framework 測試包含 parallel stream 的標的時卡住

紀錄一下最近解了幾天的問題,程式碼大體上類似 [1] 裡面的 MyRecordProcessor 和 MyRecordProcessorTest 那樣。因為本來是要建置環境比較簡單的測試來重現問題,而原本的問題是發生在開發 Kinesis 的 RecordProcessor 中遇到的,因此 [1] 的範例也沿用了這個關係。不過實際上最後發現的原因跟 Kinesis 沒有關係就是了。

問題的徵兆是,當要測試的標的內部改寫成有 parallel 的狀況時,Spock 這邊執行測試就可能會出現測試整個卡在某個地方的狀況。卡住時會完全沒有回應,但也沒有拋出錯誤或者停止執行。這個問題好像遇到的人並不是那麼多,所以可以找到的資料也非常少。不過主要參考了 [2-3],最後確認的原因是因為測試時用了 Spy。Spy 好像存在某些限制(在 [3] 的回答中有提到,雖然他沒提到到底是什麼限制),實際上似乎導致在 parallel 的狀況時,Spy 的 dynamic proxy 似乎有時會生效、但有時會無效。所以最後解決方法就是~不要用 Spy。

參考資料
  1. spock-parallel-test
  2. Spock unit test is stuck when parallel stream is used
  3. Using thread pools in a Spock Spy'd class hangs during unit tests

2020年3月1日 星期日

Vespa 入門(一):在本機建立 containerized Vespa

因為在公司持續地在用 Vespa,所以打算寫個系列文章慢慢紀錄 Vespa 的一些使用、調校經驗。(雖然說過去寫的系列文章大多…寫到第二或第三篇就………。)

本篇會紀錄在本機啟動 Vespa 的 container 的方法,通常這麼做可以在開發階段用來測試 Vespa 的語法。可以參考 Vespa 官方文件 [1]。

環境準備

依照 [1] 的描述,Docker 環境最重要的就是需要有至少 6GB 的記憶體。而在我的環境中,我是在 Windows 10 上跑 Docker 的,因此至少要是個有支援 Hyper-V 的 Windows 版本,才能夠執行 Docker。

取得 Vespa 範例

首先,Vespa 有提供一些簡單的範例 Application,可以從以下的 Github 取得。裡面有很多資料夾,包含了數種在 Vespa 官方文件中會講到的例子。

git clone https://github.com/vespa-engine/sample-apps.git

這裡假設我把這個 repository 放在 G:\git 裡。

建立並啟動 Vespa container

要啟動 Vespa 之前,我們需要執行兩個動作。第一個是先啟動一個 Vespa 的 container,這個步驟只會準備好 Vespa 需要的執行環境而已。接著第二個是部署 Vespa Application,這個動作就像是在 RDBMS 裡執行 CREATE DATABASE 一樣,是讓 Vespa 準備好 schema 並且真的啟動伺服器。

docker run --detach --name sample-vespa --hostname vespa-container --privileged --volume g:\git\vespa-sample-apps:/vespa-sample-apps --publish 8080:8080 vespaengine/vespa
docker exec sample-vespa bash -c 'curl -s --head http://localhost:19071/ApplicationStatus'

首先如前所說,我們先以 vespaengine/vespa 的 image 啟動一個 Vespa container,這裡我們把這個 container 命名為 sample-vespa,並且把資料夾 g:\git\vespa-sample-apps 掛到 container 的 /vespa-sample-apps 路徑上,方便之後我們在這個 container 中部署與啟動 Vespa。

container 開好以後,可以透過第二行指令來確認 Vespa 的狀態。不過因為 container 雖然綁了 port,但卻還沒有啟動任何服務,因此我們要確認狀態只能利用在 container 裡執行 curl 指令來確認。如果正常的話會獲得類似這樣的回覆:

HTTP/1.1 200 OK
Date: Sun, 01 Mar 2020 11:31:41 GMT
Content-Type: application/json
Content-Length: 9232

接著就是要在裡面部署和啟動指定的 Vespa Application 了。

docker exec sample-vespa bash -c '/opt/vespa/bin/vespa-deploy prepare /vespa-sample-apps/album-recommendation-selfhosted/src/main/application/ && /opt/vespa/bin/vespa-deploy activate'
curl -s --head http://localhost:8080/ApplicationStatus

這邊我們先部署 album-recommentation-selfhosted 這個 Vespa Application。因為 Vespa 的 Search Definition 是放在 src/main/application 裡的,因此在部署的時候要指定路徑是這個資料夾。第一個指令會獲得類似這樣的反應,表示已經順利地啟用 Vespa Application。

Uploading application '/vespa-sample-apps/album-recommendation-selfhosted/src/main/application/' using http://localhost:19071/application/v2/tenant/default/session
Session 3 for tenant 'default' created.
Preparing session 3 using http://localhost:19071/application/v2/tenant/default/session/3/prepared
WARNING: Host named 'vespa-container' may not receive any config since it is not a canonical hostname. Disregard this warning when testing in a Docker container.
Session 3 for tenant 'default' prepared.
Activating session 3 using http://localhost:19071/application/v2/tenant/default/session/3/active
Session 3 for tenant 'default' activated.
Checksum:   71cf99ec40a0a08c8edbd1aa3cee03ed
Timestamp:  1583062394458
Generation: 3

再使用第二個指令確認 Application 狀態時,可以獲得:

HTTP/1.1 200 OK
Date: Sun, 01 Mar 2020 11:34:39 GMT
Content-Type: application/json
Transfer-Encoding: chunked
參考資料
  1. Vespa: Quick Start

2019年12月29日 星期日

Reactive Programming(二):Flow API

本系列的文章為 Modern Java in Action / Project Reactor 的讀書筆記,因此內容可能會有點跳躍。