2026/02/02

systemd template unit service

systemd template unit 是一種樣板服務 (service template),可以用同一份 unit 檔去啟動多個獨立的 service instance。當我們需要用同一個 service daemon 啟動多個 service instance 時,就可以透過這個方式,讓 service 對應到不同的設定檔,同時並存於一台機器中。

httpd

在 /usr/lib/systemd/system 目錄,除了 httpd.service,還有 httpd@.service

  • @ 代表這個 unit 是一個「模板」。

  • %i 代表實例名稱 (instance name),會在啟動的時候被替換。

systemd template 支援一些 specifier,常見的有:

  • %i → instance name (例如 site1 / site2)

  • %I → instance name,保持大小寫

  • %n → 完整的 unit name (httpd@site1.service)

  • %p → prefix name (httpd)

httpd@service 的內容是這樣

httpd@.service
# This is a template for httpd instances.
# See httpd@.service(8) for more information.

[Unit]
Description=The Apache HTTP Server
After=network.target remote-fs.target nss-lookup.target
Documentation=man:httpd@.service(8)

[Service]
Type=notify
Environment=LANG=C
Environment=HTTPD_INSTANCE=%i
ExecStartPre=/bin/mkdir -m 710 -p /run/httpd/instance-%i
ExecStartPre=/bin/chown root.apache /run/httpd/instance-%i
ExecStart=/usr/sbin/httpd $OPTIONS -DFOREGROUND -f conf/%i.conf
ExecReload=/usr/sbin/httpd $OPTIONS -k graceful -f conf/%i.conf
# Send SIGWINCH for graceful stop
KillSignal=SIGWINCH
KillMode=mixed
PrivateTmp=true

service 會讀取 /etc/httpd/conf/%i.conf 設定檔,並將 pid 放在 /run/httpd/instance-%i

所以要產生兩個 httpd unit service 設定檔

cp /etc/httpd/conf/httpd.conf /etc/httpd/conf/site1.conf
cp /etc/httpd/conf/httpd.conf /etc/httpd/conf/site2.conf

修改 site1.conf 以下這些設定。site2.conf 就改另一個 Listen 8001,site1 改為 site2,去掉其他 Directory 的部分

Listen 8000
PidFile /run/httpd-site1.pid

DocumentRoot "/var/www/site1"

<Directory "/var/www/site1">
    Options Indexes FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>

ErrorLog "/var/log/httpd/site1_error.log"

CustomLog "/var/log/httpd/site1_access.log" combined

啟動

systemctl start httpd@site1
systemctl start httpd@site2

systemctl enable httpd@site1
systemctl enable httpd@site2

haproxy

如果是 haproxy,因為套件裡面沒有 unit service,我們需要自己製作一個

首先產生 /usr/lib/systemd/system/haproxy@.service 檔案

[Unit]
Description=HAProxy Load Balancer %i instance
After=network-online.target
Wants=network-online.target

[Service]
Environment="CONFIG=/etc/haproxy/%i.cfg" "PIDFILE=/run/haproxy-%i.pid" "CFGDIR=/etc/haproxy/conf.d.%i"
EnvironmentFile=/etc/sysconfig/haproxy.%i
ExecStartPre=/usr/sbin/haproxy -f $CONFIG -f $CFGDIR -c -q $OPTIONS
ExecStart=/usr/sbin/haproxy -Ws -f $CONFIG -f $CFGDIR -p $PIDFILE $OPTIONS
ExecReload=/usr/sbin/haproxy -f $CONFIG -f $CFGDIR -c -q $OPTIONS
ExecReload=/bin/kill -USR2 $MAINPID
SuccessExitStatus=143
KillMode=mixed
Type=notify

[Install]
WantedBy=multi-user.target

製作設定檔

cp /etc/sysconfig/haproxy /etc/sysconfig/haproxy.site1
cp /etc/sysconfig/haproxy /etc/sysconfig/haproxy.site2

製作 /etc/haproxy/sit1.cfg

global
    log 127.0.0.1 local2
    chroot /var/lib/haproxy
    pidfile /var/run/haproxy-site1.pid
    stats socket /var/run/haproxy.admin.sock mode 660 level admin

    maxconn     50000
    maxconnrate 100000
    maxsessrate 100000
    user        haproxy
    group       haproxy
    daemon
    nbproc  1
    ca-base     /etc/pki/site1
    crt-base    /etc/pki/site1
    tune.ssl.default-dh-param   2048
    # turn on stats unix socket
    stats socket /var/lib/haproxy/stats-site1

    ssl-default-bind-options no-sslv3
    ssl-default-bind-options no-sslv3 no-tlsv11 no-tlsv10

defaults
    log global
    mode    http
    option  httplog clf
    option  forwardfor
    option  dontlognull
    option  httpchk
    option  http-keep-alive
    retries 3
    maxconn 50000
    rate-limit sessions 20000
    option  http-server-close
    timeout connect 1h
    timeout client  1h
    timeout server  1h
    #timeout connect 5000
    #timeout client  50000
    #timeout server  50000
    timeout tunnel  1h

frontend http_redirect
    bind    *:80
    mode    http
    acl kill_it method TRACE
    http-request deny if kill_it
    redirect   scheme https code 301 if !{ ssl_fc }
    default_backend web_server

frontend https_switch
    bind    *:443 ssl crt server.pem ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384
    mode    http
    option  forwardfor
    reqadd  X-Forwarded-Proto:\ https

    default_backend web_server

backend web_server
    mode    http
    fullconn    50000
    balance leastconn
    option      forwardfor
    #cookie      SERVERID insert indirect nocache
    #cookie SESSIONID prefix indirect nocache
    cookie  SESSIONID prefix nocache
    http-request        set-header X-Forwarded-Port %[dst_port]
    http-request        add-header X-Forwarded-Proto https if { ssl_fc }
    #option      httpchk GET /
    option  httpchk *
    server  W01 localhost:8000 weight 10 check cookie W01 inter 5s rise 2 fall 3

製作另一個設定檔 /etc/haproxy/site2.cfg,注意要修改 bind port

然後注意,申請兩個 ssl 憑證,放到 /etc/pki/site1 跟 /etc/pki/site2

啟動

systemctl start haproxy@site1
systemctl start haproxy@site2

systemctl enable haproxy@site1
systemctl enable haproxy@site2

2026/01/26

ChatTTS

ChatTTS GitHub - 2noise/ChatTTS: A generative speech model for daily dialogue. 是支援中文與英文兩種語言的 TTS engine,特別的是,不僅只是單純的轉換為語音,還能調整音色,設定語速,增加笑聲口頭語等等功能。ChatTTS使用約100,000小時的中文和英文數據進行訓練,開源版本是一個在 40,000 小時語音資料上進行無監督微調的預訓練模型。為了限制 ChatTTS 的使用,在 40,000 小時模型的訓練過程中包含了少量高頻噪音。

文字的部分要注意有些標點符號不支援,另外還有阿拉伯數字也不支援,使用前要先把數字轉換為中文。

安裝

pip3 install torchaudio soundfile
pip3 install git+https://github.com/2noise/ChatTTS

測試

最基本的測試程式

import ChatTTS
import torch
import torchaudio

chat = ChatTTS.Chat()
chat.load(compile=False) # Set to True for better performance

texts = ["In some versions of torchaudio, the first line works but in other versions, so does the second line.", "I needed to install both torchaudio and soundfile in conda isolated environment"]

wavs = chat.infer(texts)

for i in range(len(wavs)):
    """
    In some versions of torchaudio, the first line works but in other versions, so does the second line.
    """
    try:
        torchaudio.save(f"basic_output{i}.wav", torch.from_numpy(wavs[i]).unsqueeze(0), 24000)
    except:
        torchaudio.save(f"basic_output{i}.wav", torch.from_numpy(wavs[i]), 24000)

以下是一個可調整參數的測試程式

import ChatTTS
import torch
import torchaudio

chat = ChatTTS.Chat()
chat.load(compile=False) # Set to True for better performance

# 需要转化为音频的文本内容
text = '重返白宮才要滿三週的川普已經為了削減聯邦支出而發布一系列行政命令。'


###################################
# Sample a speaker from Gaussian.

rand_spk = chat.sample_random_speaker()
print(rand_spk) # save it for later timbre recovery

params_infer_code = ChatTTS.Chat.InferCodeParams(
    spk_emb = rand_spk, # add sampled speaker
    temperature = .3,   # using custom temperature
    top_P = 0.7,        # top P decode
    top_K = 20,         # top K decode
)

###################################
# For sentence level manual control.

# use oral_(0-9), laugh_(0-2), break_(0-7)
# to generate special token in text to synthesize.
params_refine_text = ChatTTS.Chat.RefineTextParams(
    prompt='[oral_2][laugh_0][break_6]',
)

wavs = chat.infer(
    text,
    use_decoder=True,
    params_refine_text=params_refine_text,
    params_infer_code=params_infer_code,
)

for i in range(len(wavs)):
    """
    In some versions of torchaudio, the first line works but in other versions, so does the second line.
    """
    try:
        torchaudio.save(f"output{i}.wav", torch.from_numpy(wavs[i]).unsqueeze(0), 24000)
    except:
        torchaudio.save(f"output{i}.wav", torch.from_numpy(wavs[i]), 24000)

固定音色

程式是以亂數產生一個 speaker。參考# ChatTTS的进阶用法,手把手带你实现个性化配音,音色、语速、停顿,口语 的說明,speaker 是一個 768 維度的向量,所以可以儲存後,然後複製到程式內,就可以固定音色。

例如可用以下的 speaker 向量,產生一個女性音色的語音。

speaker_vector = '-4.741,0.419,-3.355,3.652,-1.682,-1.254,9.719,1.436,0.871,12.334,-0.175,-2.653,-3.132,0.525,1.573,-0.351,0.030,-3.154,0.935,-0.111,-6.306,-1.840,-0.818,9.773,-1.842,-3.433,-6.200,-4.311,1.162,1.023,11.552,2.769,-2.408,-1.494,-1.143,12.412,0.832,-1.203,5.425,-1.481,0.737,-1.487,6.381,5.821,0.599,6.186,5.379,-2.141,0.697,5.005,-4.944,0.840,-4.974,0.531,-0.679,2.237,4.360,0.438,2.029,1.647,-2.247,-1.716,6.338,1.922,0.731,-2.077,0.707,4.959,-1.969,5.641,2.392,-0.953,0.574,1.061,-9.335,0.658,-0.466,4.813,1.383,-0.907,5.417,-7.383,-3.272,-1.727,2.056,1.996,2.313,-0.492,3.373,0.844,-8.175,-0.558,0.735,-0.921,8.387,-7.800,0.775,1.629,-6.029,0.709,-2.767,-0.534,2.035,2.396,2.278,2.584,3.040,-6.845,7.649,-2.812,-1.958,8.794,2.551,3.977,0.076,-2.073,-4.160,0.806,3.798,-1.968,-4.690,5.702,-4.376,-2.396,1.368,-0.707,4.930,6.926,1.655,4.423,-1.482,-3.670,2.988,-3.296,0.767,3.306,1.623,-3.604,-2.182,-1.480,-2.661,-1.515,-2.546,3.455,-3.500,-3.163,-1.376,-12.772,1.931,4.422,6.434,-0.386,-0.704,-2.720,2.177,-0.666,12.417,4.228,0.823,-1.740,1.285,-2.173,-4.285,-6.220,2.479,3.135,-2.790,1.395,0.946,-0.052,9.148,-2.802,-5.604,-1.884,1.796,-0.391,-1.499,0.661,-2.691,0.680,0.848,3.765,0.092,7.978,3.023,2.450,-15.073,5.077,3.269,2.715,-0.862,2.187,13.048,-7.028,-1.602,-6.784,-3.143,-1.703,1.001,-2.883,0.818,-4.012,4.455,-1.545,-14.483,-1.008,-3.995,2.366,3.961,1.254,-0.458,-1.175,2.027,1.830,2.682,0.131,-1.839,-28.123,-1.482,16.475,2.328,-13.377,-0.980,9.557,0.870,-3.266,-3.214,3.577,2.059,1.676,-0.621,-6.370,-2.842,0.054,-0.059,-3.179,3.182,3.411,4.419,-1.688,-0.663,-5.189,-5.542,-1.146,2.676,2.224,-5.519,6.069,24.349,2.509,4.799,0.024,-2.849,-1.192,-16.989,1.845,6.337,-1.936,-0.585,1.691,-3.564,0.931,0.223,4.314,-2.609,0.544,-1.931,3.604,1.248,-0.852,2.991,-1.499,-3.836,1.774,-0.744,0.824,7.597,-1.538,-0.009,0.494,-2.253,-1.293,-0.475,-3.816,8.165,0.285,-3.348,3.599,-4.959,-1.498,-1.492,-0.867,0.421,-2.191,-1.627,6.027,3.667,-21.459,2.594,-2.997,5.076,0.197,-3.305,3.998,1.642,-6.221,3.177,-3.344,5.457,0.671,-2.765,-0.447,1.080,2.504,1.809,1.144,2.752,0.081,-3.700,0.215,-2.199,3.647,1.977,1.326,3.086,34.789,-1.017,-14.257,-3.121,-0.568,-0.316,11.455,0.625,-6.517,-0.244,-8.490,9.220,0.068,-2.253,-1.485,3.372,2.002,-3.357,3.394,1.879,16.467,-2.271,1.377,-0.611,-5.875,1.004,12.487,2.204,0.115,-4.908,-6.992,-1.821,0.211,0.540,1.239,-2.488,-0.411,2.132,2.130,0.984,-10.669,-7.456,0.624,-0.357,7.948,2.150,-2.052,3.772,-4.367,-11.910,-2.094,3.987,-1.565,0.618,1.152,1.308,-0.807,1.212,-4.476,0.024,-6.449,-0.236,5.085,1.265,-0.586,-2.313,3.642,-0.766,3.626,6.524,-1.686,-2.524,-0.985,-6.501,-2.558,0.487,-0.662,-1.734,0.275,-9.230,-3.785,3.031,1.264,15.340,2.094,1.997,0.408,9.130,0.578,-2.239,-1.493,11.034,2.201,6.757,3.432,-4.133,-3.668,2.099,-6.798,-0.102,2.348,6.910,17.910,-0.779,4.389,1.432,-0.649,5.115,-1.064,3.580,4.129,-4.289,-2.387,-0.327,-1.975,-0.892,5.327,-3.908,3.639,-8.247,-1.876,-10.866,2.139,-3.932,-0.031,-1.444,0.567,-5.543,-2.906,1.399,-0.107,-3.044,-4.660,-1.235,-1.011,9.577,2.294,6.615,-1.279,-2.159,-3.050,-6.493,-7.282,-8.546,5.393,2.050,10.068,3.494,8.810,2.820,3.063,0.603,1.965,2.896,-3.049,7.106,-0.224,-1.016,2.531,-0.902,1.436,-1.843,1.129,6.746,-2.184,0.801,-0.965,-7.555,-18.409,6.176,-3.706,2.261,4.158,-0.928,2.164,-3.248,-4.892,-0.008,-0.521,7.931,-10.693,4.320,-0.841,4.446,-1.591,-0.702,4.075,3.323,-3.406,-1.198,-5.518,-0.036,-2.247,-2.638,2.160,-9.644,-3.858,2.402,-2.640,1.683,-0.961,-3.076,0.226,5.106,0.712,0.669,2.539,-4.340,-0.892,0.732,0.775,-2.757,4.365,-2.368,5.368,0.342,-0.655,0.240,0.775,3.686,-4.008,16.296,4.973,1.851,4.747,0.652,-2.117,6.470,2.189,-8.467,3.236,3.745,-1.332,3.583,-2.504,5.596,-2.440,0.995,-2.267,-3.322,3.490,1.156,1.716,0.669,-3.640,-1.709,5.055,6.265,-3.963,2.863,14.129,5.180,-3.590,0.393,0.234,-3.978,6.946,-0.521,1.925,-1.497,-0.283,0.895,-3.969,5.338,-1.808,-3.578,2.699,2.728,-0.895,-2.175,-2.717,2.574,4.571,1.131,2.187,3.620,-0.388,-3.685,0.979,2.731,-2.164,1.628,-1.006,-7.766,-11.033,-10.985,-2.413,-1.967,0.790,0.826,-1.623,-1.783,3.021,1.598,-0.931,-0.605,-1.684,1.408,-2.771,-2.354,5.564,-2.296,-4.774,-2.830,-5.149,2.731,-3.314,-1.002,3.522,3.235,-1.598,1.923,-2.755,-3.900,-3.519,-1.673,-2.049,-10.404,6.773,1.071,0.247,1.120,-0.794,2.187,-0.189,-5.591,4.361,1.772,1.067,1.895,-5.649,0.946,-2.834,-0.082,3.295,-7.659,-0.128,2.077,-1.638,0.301,-0.974,4.331,11.711,4.199,1.545,-3.236,-4.404,-1.333,0.623,1.414,-0.240,-0.816,-0.808,-1.382,0.632,-5.238,0.120,10.634,-2.026,1.702,-0.469,1.252,1.173,3.015,-8.798,1.633,-5.323,2.149,-6.481,11.635,3.072,5.642,5.252,4.702,-3.523,-0.594,4.150,1.392,0.554,-4.377,3.646,-0.884,1.468,0.779,2.372,-0.101,-5.702,0.539,-0.440,5.149,-0.011,-1.899,-1.349,-0.355,0.076,-0.100,-0.004,5.346,6.276,0.966,-3.138,-2.633,-3.124,3.606,-3.793,-3.332,2.359,-0.739,-3.301,-2.775,-0.491,3.283,-1.394,-1.883,1.203,1.097,2.233,2.170,-2.980,-15.800,-6.791,-0.175,-4.600,-3.840,-4.179,6.568,5.935,-0.431,4.623,4.601,-1.726,0.410,2.591,4.016,8.169,1.763,-3.058,-1.340,6.276,4.682,-0.089,1.301,-4.817' # 768维向量
rand_spk = torch.tensor([float(x) for x in speaker_vector.split(',')])

ChatTTS ### 稳定音色查找与音色可查詢並下載已經測試過提供的音色檔案,例如,下載一個音色檔案:seed_2147_restored_emb.pt 後,用以下程式載入該音色檔案,因為是使用 CPU,沒有 GPU,故需要加上 map_location=torch.device('cpu')

rand_spk = torch.load('seed_2147_restored_emb.pt', map_location=torch.device('cpu'))

特效

增加笑聲或停頓,要在文字中間加上

  • [laugh] 代表笑聲

  • [uv_break] 代表停頓

通過 params_refine_text 中的 prompt 參數可以控制笑聲和停頓的強度:

笑聲:laugh_(0-2):laugh_0、laugh_1、laugh_2(笑聲愈加強烈) 停頓:break_(0-7):break_0 至 break_7(停頓逐漸明顯)

實際合成語音時,會自動加上一些停頓,可加上 skip_refine_text=True,強制以原始的方式合成

chat.infer([text], skip_refine_text=True, params_refine_text={"prompt": '[oral_2][laugh_0][break_6]'})

References

ChatTTS: Text-to-Speech For Chat

ChatTTS使用demo示例(包含长文本生成语音、固定音色pt文件)

# ChatTTS使用技巧:如何精细化控制语气、音色、语速 附一键整合包!

## ChatTTS归因分析-音色性别转换方法

# 揭秘ChatTTS:高可控语音合成神器上手实录 带你玩转ChatTTS!

2026/01/19

CRDT

CDRT conflict-free replicated data type 無衝突複製資料類型是一種可以在網路中的多台電腦上複製的資料結構,每一個副本可獨立各自更新,不需要在副本之間進行協調,已透過數學確認可解決可能出現的不一致問題。

對於不同節點上的共用資料,如果在某一個節點上更新了資料,會產生不一致性,如果節點之間沒有協調更新的權限而產生資料衝突,就可能需要放棄一部分更新,以達到最終的一致性,分散式計算都集中在如何防止複製資料的並行更新問題上。

另一種方式 Optimistic Replication,是讓各節點都能做更新,允許所有更新都能同時執行,然後再來考慮如何合併解決衝突問題,CRDT 是一種資料結構,可在不同的副本上進行更新,最後透過合併解決衝突問題。

CRDT 種類

基於狀態的 CRDT 比較容易實作,但需要每個 CRDT 都要將狀態傳給其他副本,傳輸資料耗費較大,基於操作的 CRDT 只需要傳送更新的動作記錄。

基於操作的 CRDT

也稱為 交換性複製資料類型(commutative replicated data types,或CmRDT)

只傳輸更新操作來傳播狀態,必須確保操作過程都有傳遞給每一個資料副本,可不依照順序,但不能重複。

基於狀態的 CRDT

被稱為收斂複製資料類型(convergent replicated data types,或CvRDT)

必須將完整的本地端狀態,重送給其他副本。

State-Based LWW-Element-Graph CRDT

GitHub - juliuskrah/crdt-java: A minimal CRDT implementation 這是一個簡易的 java library,實作了 CvRDT,用 graph 方式,連結不同的節點。

當兩個節點沒有連線時,就是 split-brain 狀態,CRDT 可在節點連接後,讓不同的副本資料合併達到一致性。

這邊測試四種演算法

  • Grow-Only Set:會一直不斷增加的 Set,最終不同節點的 Set 會合併再一起

  • Increment-Only Counter: 結果會是所有節點的加總總和

  • PN Counter:結果是所有節點的 加總總和 - 減法總和

  • Last-Writer-Wins Register:在合併時,只會保留最新的異動內容。同時發生的操作會被丟棄

<!-- Experimental CRDT-implementations for the JVM -->
        <dependency>
            <groupId>com.netopyr.wurmloch</groupId>
            <artifactId>wurmloch-crdt</artifactId>
            <version>0.1.0</version>
        </dependency>
import com.netopyr.wurmloch.crdt.GCounter;
import com.netopyr.wurmloch.crdt.GSet;
import com.netopyr.wurmloch.crdt.LWWRegister;
import com.netopyr.wurmloch.crdt.PNCounter;
import com.netopyr.wurmloch.store.LocalCrdtStore;
import org.junit.Test;

import static org.junit.Assert.*;

public class CRDTTest {
    @Test
    public void gset() {
        LocalCrdtStore crdtStore1 = new LocalCrdtStore();
        LocalCrdtStore crdtStore2 = new LocalCrdtStore();
        crdtStore1.connect(crdtStore2);

        GSet<String> replica1 = crdtStore1.createGSet("fruit");
        GSet<String> replica2 = crdtStore2.<String>findGSet("fruit").get();

        replica1.add("apple");
        replica2.add("banana");

        // 確認replica1, replica2 都有 "apple" 及  “”banana
        assertTrue( replica1.contains("apple") );
        assertTrue( replica1.contains("banana") );
        assertTrue( replica2.contains("apple") );
        assertTrue( replica2.contains("banana") );

        // 刻意斷線
        crdtStore1.disconnect(crdtStore2);
        // 異動 replica1, replica2
        replica1.add("strawberry");
        replica2.add("pear");

        assertTrue( replica1.contains("strawberry") );
        assertFalse( replica2.contains("strawberry") );
        assertFalse( replica1.contains("pear") );
        assertTrue( replica2.contains("pear") );

        // 連線
        crdtStore1.connect(crdtStore2);

        assertTrue( replica1.contains("strawberry") );
        assertTrue( replica2.contains("strawberry") );
        assertTrue( replica1.contains("pear") );
        assertTrue( replica2.contains("pear") );
    }

    @Test
    public void gcounter() {
        // 結果會是所有節點的 sum
        LocalCrdtStore crdtStore1 = new LocalCrdtStore();
        LocalCrdtStore crdtStore2 = new LocalCrdtStore();
        crdtStore1.connect(crdtStore2);

        GCounter replica1 = crdtStore1.createGCounter("counter");
        GCounter replica2 = crdtStore2.findGCounter("counter").get();

        replica1.increment();
        replica2.increment(2L);

        assertEquals(3L, replica1.get());
        assertEquals(3L, replica2.get());

        // 斷線
        crdtStore1.disconnect(crdtStore2);

        replica1.increment(3L);
        replica2.increment(5L);

        assertEquals(6L, replica1.get());
        assertEquals(8L, replica2.get());

        // connect
        crdtStore1.connect(crdtStore2);
        assertEquals(11L, replica1.get());
        assertEquals(11L, replica2.get());
    }

    @Test
    public void pncounter() {
        // 結果是所有節點的 加總總和 - 減法總和
        LocalCrdtStore crdtStore1 = new LocalCrdtStore();
        LocalCrdtStore crdtStore2 = new LocalCrdtStore();
        crdtStore1.connect(crdtStore2);

        PNCounter replica1 = crdtStore1.createPNCounter("pncounter");
        PNCounter replica2 = crdtStore2.findPNCounter("pncounter").get();

        replica1.increment();
        replica2.decrement(2L);

        assertEquals(-1L, replica1.get());
        assertEquals(-1L, replica2.get());

        // disconnect
        crdtStore1.disconnect(crdtStore2);

        replica1.decrement(3L);
        replica2.increment(5L);

        assertEquals(-4L, replica1.get());
        assertEquals(4L, replica2.get());

        // connect
        crdtStore1.connect(crdtStore2);

        assertEquals(1L, replica1.get());
        assertEquals(1L, replica2.get());
    }

    @Test
    public void lwwregister() {
        // 在合併時,只會保留最新的異動內容。同時發生的操作會被丟棄
        LocalCrdtStore crdtStore1 = new LocalCrdtStore("crdt");
        LocalCrdtStore crdtStore2 = new LocalCrdtStore("crdt");
        crdtStore1.connect(crdtStore2);

        LWWRegister<String> replica1 = crdtStore1.createLWWRegister("lwwregister");
        LWWRegister<String> replica2 = crdtStore2.<String>findLWWRegister("lwwregister").get();

        replica1.set("apple");
        replica2.set("banana");

        assertEquals("banana", replica1.get());
        assertEquals("banana", replica2.get());

        //disconnect
        crdtStore1.disconnect(crdtStore2);

        replica1.set("strawberry");
        replica2.set("pear");

        assertEquals("strawberry", replica1.get());
        assertEquals("pear", replica2.get());

        // connect
        crdtStore1.connect(crdtStore2);

        // buggy:應該要是 pear
        assertEquals("strawberry", replica1.get());
        assertEquals("strawberry", replica2.get());
    }
}

References

無衝突複製資料類型 - 維基百科,自由的百科全書

# CRDT — 將非同步資料整合

CRDT - HackMD

Introduction to Conflict-Free Replicated Data Types | Baeldung

GitHub - juliuskrah/crdt-java: A minimal CRDT implementation