snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
test / Lint (push) Failing after 3s
test / Test with Go 1.26.8 (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:17:21 +05:00
commit b352a9a30d
472 changed files with 108375 additions and 0 deletions

1
misc/README.md vendored Normal file
View File

@@ -0,0 +1 @@
This folder contains various tools, deploy scripts etc for Centrifugo.

View File

@@ -0,0 +1,78 @@
import ws from 'k6/ws';
import { check } from 'k6';
import crypto from "k6/crypto";
import encoding from "k6/encoding";
const hmacSecret = "secret";
const algToHash = {
HS256: "sha256",
HS384: "sha384",
HS512: "sha512"
};
function sign(data, hashAlg, secret) {
let hasher = crypto.createHMAC(hashAlg, secret);
hasher.update(data);
// Some manual base64 encoding as `Hasher.digest(encodingType)` doesn't support that encoding type yet.
return hasher.digest("base64").replace(/\//g, "_").replace(/\+/g, "-").replace(/=/g, "");
}
function encode(payload, secret, algorithm) {
algorithm = algorithm || "HS256";
let header = encoding.b64encode(JSON.stringify({ typ: "JWT", alg: algorithm }), "rawurl");
payload = encoding.b64encode(JSON.stringify(payload), "rawurl");
let sig = sign(header + "." + payload, algToHash[algorithm], secret);
return [header, payload, sig].join(".");
}
// Generate a token for each user separately using HMAC JWT
function generateToken(userId) {
const payload = {
sub: userId,
exp: Math.floor(Date.now() / 1000) + 60, // Expires in 60 seconds
};
return encode(payload, hmacSecret, "HS256")
}
export let options = {
stages: [
{ duration: "10s", target: 1000 },
{ duration: "50s", target: 1000 },
]
};
export default function () {
const url = 'ws://localhost:8000/connection/websocket';
const user = `user_${__VU}_${__ITER}`;
const token = generateToken(user);
const response = ws.connect(url, {}, function (socket) {
socket.on('open', () => {
const connectCommand = JSON.stringify({ id: 1, connect: { token: token } });
const subscribeCommand = JSON.stringify({ id: 2, subscribe: { channel: 'test' } });
socket.send(connectCommand + "\n" + subscribeCommand);
});
socket.on('message', (message) => {
// Respond to server pings.
const substrings = message.split("\n");
if (substrings.includes("{}")) {
socket.send('{}');
}
});
socket.on('close', () => {});
socket.on('error', (error) => {
console.log('WebSocket error: ', error);
});
socket.setTimeout(() => {
socket.close();
}, 60000);
});
check(response, { "status is 101": (r) => r && r.status === 101 });
}

View File

@@ -0,0 +1,27 @@
A simple script which demonstrates benchmarking Centrifugo with [k6](https://k6.io/). You can use it as a starting point.
It connects to Centrifugo with JWT per user, subscribes to the `test` channel, waits some time and exits.
## Running test
Run Centrifugo with config like this:
```json
{
"token_hmac_secret_key": "secret",
"admin": true,
"admin_password": "password",
"admin_secret": "0453661f-8b95-4f75-ab49-cfb2e3b2024f",
"api_key": "7d8cee29-f5c0-419e-ae70-760767b21f8e",
"allow_subscribe_for_client": true,
"allowed_origins": []
}
```
Then:
```
k6 run benchmark.js
```
Make sure [your system is tuned](https://centrifugal.dev/docs/server/infra_tuning) for dealing with many connections.

View File

@@ -0,0 +1,48 @@
import ws from 'k6/ws';
import { check } from 'k6';
export let options = {
stages: [
// Ramp up to 5000 connections over 60 seconds
{ duration: '60s', target: 5000 },
// Hold 5000 connections for 4 minutes
{ duration: '4m', target: 5000 },
// Ramp down to 0 connections
{ duration: '10s', target: 0 },
],
};
export default function () {
const url = 'ws://localhost:8000/connection/uni_websocket?cf_connect={}';
const response = ws.connect(url, {}, function (socket) {
socket.on('open', function open() {
console.log('WebSocket connection established');
});
socket.on('message', function message(data) {
console.log('Received message:', data);
});
socket.on('close', function close() {
console.log('WebSocket connection closed');
});
socket.on('error', function error(e) {
console.log('WebSocket error:', e.error());
});
// Keep the connection alive for the duration of the test
// The connection will be closed when the VU iteration ends
socket.setTimeout(function () {
socket.close();
}, 300000); // 5 minutes timeout as safety measure
// Send a ping every 30 seconds to keep connection active
socket.setInterval(function () {
socket.ping();
}, 30000);
});
check(response, { 'status is 101': (r) => r && r.status === 101 });
}

View File

@@ -0,0 +1,5 @@
Makes 5k conns to unidirectional websocket endpoint during 1 min, waits 4 mins, stops.
```
k6 run benchmark.js
```

View File

@@ -0,0 +1 @@
Various helpers for benchmarking Centrifugo.

11
misc/clickhouse_cluster/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
.vscode
.idea
zookeper/
clickhouse01/
clickhouse01_db/
clickhouse02/
clickhouse02_db/
clickhouse03/
clickhouse03_db/
clickhouse04/
clickhouse04_db/

View File

@@ -0,0 +1,28 @@
.PHONY: config
config:
rm -rf clickhouse01 clickhouse02 clickhouse03 clickhouse04
mkdir -p clickhouse01 clickhouse02 clickhouse03 clickhouse04
rm -rf clickhouse01_db clickhouse02_db clickhouse03_db clickhouse04_db
mkdir -p clickhouse01_db clickhouse02_db clickhouse03_db clickhouse04_db
rm -rf zookeeper
mkdir -p zookeeper
REPLICA=01 SHARD=01 envsubst < config.xml > clickhouse01/config.xml
REPLICA=02 SHARD=01 envsubst < config.xml > clickhouse02/config.xml
REPLICA=03 SHARD=02 envsubst < config.xml > clickhouse03/config.xml
REPLICA=04 SHARD=02 envsubst < config.xml > clickhouse04/config.xml
cp users.xml clickhouse01/users.xml
cp users.xml clickhouse02/users.xml
cp users.xml clickhouse03/users.xml
cp users.xml clickhouse04/users.xml
.PHONY: up
up:
docker-compose up -d
.PHONY: start
start:
docker-compose start
.PHONY: down
down:
docker-compose down

View File

@@ -0,0 +1,82 @@
<?xml version="1.0"?>
<company>
<logger>
<level>debug</level>
<console>true</console>
<log remove="remove"/>
<errorlog remove="remove"/>
</logger>
<query_log>
<database>system</database>
<table>query_log</table>
</query_log>
<listen_host>0.0.0.0</listen_host>
<http_port>8123</http_port>
<tcp_port>9000</tcp_port>
<interserver_http_host>clickhouse${REPLICA}</interserver_http_host>
<interserver_http_port>9009</interserver_http_port>
<max_connections>4096</max_connections>
<keep_alive_timeout>3</keep_alive_timeout>
<max_concurrent_queries>100</max_concurrent_queries>
<uncompressed_cache_size>8589934592</uncompressed_cache_size>
<mark_cache_size>5368709120</mark_cache_size>
<path>/var/lib/clickhouse/</path>
<tmp_path>/var/lib/clickhouse/tmp/</tmp_path>
<user_files_path>/var/lib/clickhouse/user_files/</user_files_path>
<users_config>users.xml</users_config>
<default_profile>default</default_profile>
<default_database>default</default_database>
<timezone>Europe/Moscow</timezone>
<mlock_executable>false</mlock_executable>
<remote_servers>
<centrifugo_cluster>
<shard>
<internal_replication>true</internal_replication>
<replica>
<host>clickhouse01</host>
<port>9000</port>
</replica>
<replica>
<host>clickhouse02</host>
<port>9000</port>
</replica>
</shard>
<shard>
<internal_replication>true</internal_replication>
<replica>
<host>clickhouse03</host>
<port>9000</port>
</replica>
<replica>
<host>clickhouse04</host>
<port>9000</port>
</replica>
</shard>
</centrifugo_cluster>
</remote_servers>
<zookeeper>
<node index="1">
<host>zookeeper</host>
<port>2181</port>
</node>
</zookeeper>
<macros>
<cluster>centrifugo_cluster</cluster>
<shard>${SHARD}</shard>
<replica>clickhouse${REPLICA}</replica>
</macros>
<distributed_ddl>
<path>/clickhouse/task_queue/ddl</path>
</distributed_ddl>
<format_schema_path>/var/lib/clickhouse/format_schemas/</format_schema_path>
</company>

View File

@@ -0,0 +1,68 @@
version: '3.8'
services:
zookeeper:
image: zookeeper:latest
container_name: zookeeper
hostname: zookeeper
environment:
ALLOW_ANONYMOUS_LOGIN: "yes"
healthcheck:
test: echo "ruok" | nc -w 2 localhost 2181
interval: 5s
timeout: 10s
retries: 10
volumes:
- ${PWD}/zookeeper:/data
- ${PWD}/zookeeper:/logs
clickhouse01:
image: clickhouse/clickhouse-server:23-alpine
container_name: clickhouse01
hostname: clickhouse01
ports:
- "127.0.0.1:8123:8123"
- "127.0.0.1:9000:9000"
volumes:
- ${PWD}/clickhouse01:/etc/clickhouse-server
- ${PWD}/clickhouse01_db:/var/lib/clickhouse
depends_on:
zookeeper:
condition: service_healthy
clickhouse02:
image: clickhouse/clickhouse-server:23-alpine
container_name: clickhouse02
hostname: clickhouse02
ports:
- "127.0.0.1:8124:8123"
- "127.0.0.1:9001:9000"
volumes:
- ${PWD}/clickhouse02:/etc/clickhouse-server
- ${PWD}/clickhouse02_db:/var/lib/clickhouse
depends_on:
zookeeper:
condition: service_healthy
clickhouse03:
image: clickhouse/clickhouse-server:23-alpine
container_name: clickhouse03
hostname: clickhouse03
ports:
- "127.0.0.1:8125:8123"
- "127.0.0.1:9002:9000"
volumes:
- ${PWD}/clickhouse03:/etc/clickhouse-server
- ${PWD}/clickhouse03_db:/var/lib/clickhouse
depends_on:
zookeeper:
condition: service_healthy
clickhouse04:
image: clickhouse/clickhouse-server:23-alpine
container_name: clickhouse04
hostname: clickhouse04
ports:
- "127.0.0.1:8126:8123"
- "127.0.0.1:9003:9000"
volumes:
- ${PWD}/clickhouse04:/etc/clickhouse-server
- ${PWD}/clickhouse04_db:/var/lib/clickhouse
depends_on:
zookeeper:
condition: service_healthy

View File

@@ -0,0 +1,15 @@
# Clickhouse Cluster
Clickhouse cluster with 2 shards and 2 replicas built with docker-compose.
## Run
```
make config
docker compose up
```
## Profiles
- `default` - no password
- `admin` - password `123`

View File

@@ -0,0 +1,43 @@
<?xml version="1.0"?>
<company>
<profiles>
<default>
<max_memory_usage>10000000000</max_memory_usage>
<use_uncompressed_cache>0</use_uncompressed_cache>
<load_balancing>in_order</load_balancing>
<log_queries>1</log_queries>
</default>
</profiles>
<users>
<default>
<password></password>
<profile>default</profile>
<networks>
<ip>::/0</ip>
</networks>
<quota>default</quota>
</default>
<admin>
<password>123</password>
<profile>default</profile>
<networks>
<ip>::/0</ip>
</networks>
<quota>default</quota>
</admin>
</users>
<quotas>
<default>
<interval>
<duration>3600</duration>
<queries>0</queries>
<errors>0</errors>
<result_rows>0</result_rows>
<read_rows>0</read_rows>
<execution_time>0</execution_time>
</interval>
</default>
</quotas>
</company>

113
misc/kafka_producer/main.go Normal file
View File

@@ -0,0 +1,113 @@
// kafka_producer produces publish commands to a Kafka topic for Centrifugo to consume.
//
// Usage:
//
// go run ./misc/kafka_producer/ -broker localhost:29092 -topic centrifugo -channel test -interval 1s
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/twmb/franz-go/pkg/kadm"
"github.com/twmb/franz-go/pkg/kgo"
)
type publishRequest struct {
Channel string `json:"channel"`
Data json.RawMessage `json:"data"`
}
type methodPayload struct {
Method string `json:"method"`
Payload json.RawMessage `json:"payload"`
}
func main() {
broker := flag.String("broker", "localhost:29092", "Kafka broker address")
topic := flag.String("topic", "centrifugo", "Kafka topic to produce to")
channel := flag.String("channel", "test", "Centrifugo channel to publish to")
interval := flag.Duration("interval", time.Second, "Interval between messages")
count := flag.Int("count", 0, "Number of messages to send (0 = unlimited)")
flag.Parse()
client, err := kgo.NewClient(kgo.SeedBrokers(*broker))
if err != nil {
log.Fatal("error creating Kafka client:", err)
}
defer client.Close()
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
// Ensure topic exists (use a separate client since kadm.Close closes the underlying kgo client).
admClient, err := kgo.NewClient(kgo.SeedBrokers(*broker))
if err != nil {
log.Fatal("error creating admin client:", err)
}
adm := kadm.NewClient(admClient)
resp, err := adm.CreateTopics(ctx, 1, 1, nil, *topic)
if err != nil {
log.Fatal("error creating topic:", err)
}
for _, t := range resp.Sorted() {
if t.Err != nil && !strings.Contains(t.Err.Error(), "TOPIC_ALREADY_EXISTS") {
log.Fatalf("error creating topic %s: %v", t.Topic, t.Err)
}
log.Printf("topic %s ready", t.Topic)
}
adm.Close()
i := 0
for {
i++
if *count > 0 && i > *count {
break
}
data, _ := json.Marshal(map[string]interface{}{
"message": fmt.Sprintf("hello #%d", i),
"ts": time.Now().UnixMilli(),
})
publishReq, _ := json.Marshal(publishRequest{
Channel: *channel,
Data: data,
})
msg, _ := json.Marshal(methodPayload{
Method: "publish",
Payload: publishReq,
})
record := &kgo.Record{
Topic: *topic,
Value: msg,
}
res := client.ProduceSync(ctx, record)
err := res.FirstErr()
if err != nil {
if ctx.Err() != nil {
break
}
log.Println("error producing:", err)
} else {
log.Printf("produced message #%d to topic=%s channel=%s %s (offset %d)", i, *topic, *channel, string(data), res[0].Record.Offset)
}
select {
case <-ctx.Done():
return
case <-time.After(*interval):
}
}
}

View File

@@ -0,0 +1,32 @@
Produce messages to Kafka topic to quickly test out Centrifugo Kafka consumer.
Run Centrifugo with:
```json
{
"consumers": [
{
"name": "kafka_test",
"enabled": true,
"type": "kafka",
"kafka": {
"brokers": ["localhost:29092"],
"topics": ["centrifugo"],
"consumer_group": "centrifugo"
}
}
]
}
```
Use `docker-compose.yml` of Centrifugo:
```
docker compose up kafka
```
And:
```
go run ./misc/kafka_producer/
```

View File

@@ -0,0 +1,70 @@
#!/bin/bash
# Function to check if a NATS node is up
wait_for_nats() {
local port=$1
while ! nc -z localhost "$port" > /dev/null 2>&1; do
echo "Waiting for NATS node on port $port to start..."
sleep 1
done
echo "NATS node on port $port is available."
}
# Array to hold PIDs of NATS server processes
declare -a nats_pids
# Function to start NATS cluster nodes
start_nats_cluster() {
local nodes=$1
local base_port=4222
local cluster_port_base=5222
local log_dir="./nats_logs"
local cluster_nodes=()
# Create log directory, clean previous
rm -rf "$log_dir"
mkdir -p "$log_dir"
# Start NATS nodes
for ((i=0; i<nodes; i++)); do
local port=$((base_port + i))
local cluster_port=$((cluster_port_base + i))
local log_file="$log_dir/nats_node_${port}.log"
# Start a NATS server
nats-server --port "$port" --config server.conf --cluster nats://localhost:"$cluster_port" \
--routes nats://localhost:"$cluster_port_base" \
> "$log_file" 2>&1 &
nats_pids[i]=$!
cluster_nodes+=("localhost:$port")
done
# Wait for all nodes to become available
for port in "${cluster_nodes[@]}"; do
wait_for_nats "${port##*:}"
done
}
# Function to shut down NATS cluster nodes
shutdown_nats_cluster() {
echo "Shutting down NATS cluster nodes..."
for pid in "${nats_pids[@]}"; do
kill "$pid"
done
}
# Trap Ctrl+C (SIGINT) to call shutdown function
trap shutdown_nats_cluster SIGINT
# Check if the number of nodes is provided
if [ -z "$1" ]; then
echo "Usage: $0 <number_of_nodes>"
exit 1
fi
# Start the NATS cluster
start_nats_cluster "$1"
# Wait until a keyboard interrupt (Ctrl+C) is detected
echo "NATS cluster is running. Press Ctrl+C to stop."
wait

View File

@@ -0,0 +1,7 @@
accounts: {
$SYS: {
users: [
{user: admin, password: password}
]
}
}

View File

@@ -0,0 +1,24 @@
[Unit]
Description=Centrifugo real-time messaging server
Documentation=https://github.com/centrifugal/centrifugo
# start once the network and logging subsystems available
After=network.target
[Service]
User=centrifugo
Group=centrifugo
LimitNOFILE=65536
Environment="CONFIG=/etc/centrifugo/config.json"
EnvironmentFile=/etc/default/centrifugo
ExecStartPre=/usr/bin/centrifugo checkconfig -c $CONFIG
ExecStart=/usr/bin/centrifugo -c $CONFIG $CENTRIFUGO_OPTS
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -SIGTERM $MAINPID
TimeoutStopSec=10
KillMode=control-group
RestartSec=1
Restart=on-failure
[Install]
WantedBy=multi-user.target
Alias=centrifugo.service

View File

@@ -0,0 +1,30 @@
# Upstart configuration file for centrifugo
description "Centrifugo real-time messaging server"
author "Alexandr Emelin <frvzmb@gmail.com>"
start on runlevel [2345]
# Always restart the runner if it dies / exits
respawn
# Log to /var/log/upstart/
console log
# Run the process as centrifugo:centrifugo
setuid centrifugo
setgid centrifugo
limit nofile 65536 65536
script
CONFIG=/etc/centrifugo/config.json
CENTRIFUGO_OPTS=
if [ -f /etc/default/centrifugo ]; then
. /etc/default/centrifugo
fi
exec /usr/bin/centrifugo -c $CONFIG $CENTRIFUGO_OPTS
end script

159
misc/packaging/initd.el6.sh Normal file
View File

@@ -0,0 +1,159 @@
#!/bin/bash
#
# /etc/init.d/centrifugo
#
# Startup script for centrifugo
#
# chkconfig: 2345 20 80
# description: Starts and stops centrifugo
# Source function library.
if [ -f /etc/init.d/functions ] ; then
. /etc/init.d/functions
elif [ -f /etc/rc.d/init.d/functions ] ; then
. /etc/rc.d/init.d/functions
else
exit 1
fi
# Daemon options
CENTRIFUGO_OPTS=
# Process name
NAME=centrifugo
USER=centrifugo
GROUP=centrifugo
# Daemon name, where is the actual executable
# If the daemon is not there, then exit.
DAEMON=/usr/bin/centrifugo
# Configuration file
CONFIG=/etc/centrifugo/config.json
# Max open files
OPEN_FILE_LIMIT=65536
# Logging
if [ -z "$STDOUT" ]; then
STDOUT=/var/log/centrifugo/centrifugo.log
fi
if [ ! -f "$STDOUT" ]; then
mkdir -p $(dirname $STDOUT)
fi
if [ -z "$STDERR" ]; then
STDERR=/var/log/centrifugo/centrifugo.log
fi
if [ ! -f "$STDERR" ]; then
mkdir -p $(dirname $STDERR)
fi
lockfile="/var/lock/subsys/centrifugo"
pidfile="/var/run/centrifugo.pid"
[ -e /etc/sysconfig/centrifugo ] && . /etc/sysconfig/centrifugo
if ! [ -x $DAEMON ]; then
echo "$DAEMON binary not found."
exit 5
fi
start() {
if ! [ -f $CONFIG ]; then
echo "configuration file not found $CONFIG"
exit 6
fi
# Bump the file limits, before launching the daemon.
ulimit -n $OPEN_FILE_LIMIT
if [ $? -ne 0 ]; then
exit 1
fi
configtest || return $?
echo -n $"Starting $NAME: "
daemon --pidfile=$pidfile --user centrifugo --check=$DAEMON "nohup $DAEMON -c $CONFIG $CENTRIFUGO_OPTS >>$STDOUT 2>&1 &"
retval=$?
echo
[ $retval -eq 0 ] && touch $lockfile
pgrep -f "$DAEMON -c $CONFIG" > $pidfile
return $retval
}
stop() {
echo -n $"Stopping $NAME: "
killproc -p $pidfile $NAME -TERM
retval=$?
if [ $retval -eq 0 ]; then
if [ "$CONSOLETYPE" != "serial" ]; then
echo -en "\\033[16G"
fi
while rh_status_q
do
sleep 1
echo -n $"."
done
rm -f $lockfile
fi
echo
return $retval
}
restart() {
configtest || return $?
stop
sleep 1
start
}
reload() {
configtest || return $?
echo -n $"Reloading $NAME: "
killproc $DAEMON -HUP
sleep 1
RETVAL=$?
echo
}
configtest() {
$DAEMON checkconfig -c $CONFIG
}
rh_status() {
status -p $pidfile $NAME
}
rh_status_q() {
rh_status >/dev/null 2>&1
}
case "$1" in
start)
rh_status_q && exit 0
$1
;;
stop)
rh_status_q || exit 0
$1
;;
restart|configtest)
$1
;;
reload)
rh_status_q || exit 7
$1
;;
status|status_q)
rh_$1
;;
condrestart)
rh_status_q && restart || exit 0
;;
*)
echo $"Usage: $0 {start|stop|reload|status|restart|configtest|condrestart}"
exit 2
esac

262
misc/packaging/initd.sh Normal file
View File

@@ -0,0 +1,262 @@
#! /usr/bin/env bash
### BEGIN INIT INFO
# Provides: centrifugo
# Required-Start: $all
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start centrifugo at boot time
### END INIT INFO
# Command-line options that can be set in /etc/default/centrifugo. These will override
# any config file values. Example: CENTRIFUGO_OPTS="--engine=redis"
DEFAULT=/etc/default/centrifugo
# Daemon options
CENTRIFUGO_OPTS=
# Process name
readonly NAME=centrifugo
# Absolute path to the printf executable.
readonly PRINT=/usr/bin/printf
# Absolute path to the which executable.
readonly WHICH=/usr/bin/which
# User and group
USER=centrifugo
GROUP=centrifugo
# Daemon name, where is the actual executable
# If the daemon is not there, then exit.
DAEMON=/usr/bin/centrifugo
[ -x $DAEMON ] || exit 5
# Configuration file
CONFIG=/etc/centrifugo/config.json
# PID file for the daemon
PIDFILE=/var/run/centrifugo/centrifugo.pid
PIDDIR=`dirname $PIDFILE`
if [ ! -d "$PIDDIR" ]; then
mkdir -p $PIDDIR
chown $USER:$GROUP $PIDDIR
fi
# Max open files
OPEN_FILE_LIMIT=65536
if [ -r /lib/lsb/init-functions ]; then
source /lib/lsb/init-functions
fi
readonly EC_INVALID_ARGUMENT=2
readonly EC_SUPER_USER_ONLY=4
readonly EC_DAEMON_NOT_FOUND=5
readonly EC_RELOADING_FAILED=95
readonly EC_RESTART_STOP_FAILED=96
readonly EC_RESTART_START_FAILED=97
readonly EC_START_FAILED=98
readonly EC_STOP_FAILED=99
# Logging
if [ -z "$STDOUT" ]; then
STDOUT=/var/log/centrifugo/centrifugo.log
fi
if [ ! -f "$STDOUT" ]; then
mkdir -p $(dirname $STDOUT)
fi
if [ -z "$STDERR" ]; then
STDERR=/var/log/centrifugo/centrifugo.log
fi
if [ ! -f "$STDERR" ]; then
mkdir -p $(dirname $STDERR)
fi
# Overwrite init script variables with /etc/default/centrifugo values
if [ -r $DEFAULT ]; then
source $DEFAULT
fi
# Exit the script with an error code.
die() {
log_end_msg $1
exit $1
}
# End the script.
end() {
log_end_msg 0
exit 0
}
# Display failure message.
fail() {
log_failure_msg "${NAME}" "$1"
}
# Display success message.
ok() {
log_success_msg "${NAME}" "$1"
}
# Reloads the service.
reload_service()
{
start-stop-daemon --stop --signal HUP --pidfile $PIDFILE
if [ $? -ne 0 ]; then
fail "failed to reload"
else
ok "reloaded"
fi
}
# Starts the service.
start_service()
{
# Check if config file exist
if [ ! -r $CONFIG ]; then
fail "config file doesn't exist (or you don't have permission to view)"
return 1
fi
# Bump the file limits, before launching the daemon. These will carry over to
# launched processes.
ulimit -n $OPEN_FILE_LIMIT
if [ $? -ne 0 ]; then
fail "set open file limit to $OPEN_FILE_LIMIT"
return 1
fi
start-stop-daemon --start --chuid $GROUP:$USER --quiet --background --make-pidfile --pidfile $PIDFILE --startas /bin/bash -- -c "exec $DAEMON -c $CONFIG $CENTRIFUGO_OPTS >> $STDOUT 2>&1"
if [ $? -ne 0 ]; then
fail "failed to start"
else
ok "started"
fi
}
# Stops the service.
stop_service() {
start-stop-daemon --stop --signal QUIT --retry=TERM/30/KILL/5 --pidfile $PIDFILE
if [ $? -ne 0 ]; then
fail "failed to stop"
else
ok "stopped"
fi
}
# Load the LSB log_* functions.
INCLUDE=/lib/lsb/init-functions
if [ -r "${INCLUDE}" ]
then
. "${INCLUDE}"
else
"${PRINT}" '%s: unable to load LSB functions, cannot start service.\n' "${NAME}" 1>&2
exit ${EC_DAEMON_NOT_FOUND}
fi
# Make sure only one argument was passed to the script.
if [ $# -ne 1 ]
then
if [ $# -lt 1 -o "$1" = '' ]
then fail 'action not specified.'
else fail 'too many arguments.'
fi
usage 1>&2
die ${EC_INVALID_ARGUMENT}
fi
readonly ACTION="$1"
# Check if daemon is a recognized program and get absolute path.
readonly DAEMON=$("${WHICH}" "${NAME}")
if [ ! -x "${DAEMON}" ]
then
if [ "${ACTION}" = 'stop' ]
then
fail 'executable not found: stop request ignored'
end
else
fail "executable not found: cannot ${ACTION} service"
die ${EC_DAEMON_NOT_FOUND}
fi
fi
# Check start-stop-daemon
readonly SSD=$("${WHICH}" "start-stop-daemon")
if [ ! -x "${SSD}" ]
then
fail "start-stop-daemon required"
end
fi
# Determine the service's status.
start-stop-daemon --status --pidfile "${PIDFILE}" 2>/dev/null 1>/dev/null
readonly STATUS=$?
configtest() {
$DAEMON checkconfig -c $CONFIG
}
status_q() {
status_of_proc "${DAEMON}" "${NAME}" >/dev/null 2>&1
}
case $1 in
start)
if [ ${STATUS} -eq 0 ]
then
ok 'already started.'
else
start_service || die ${EC_START_FAILED}
fi
;;
stop)
if [ ${STATUS} -eq 0 ]
then
stop_service || die ${EC_STOP_FAILED}
else
ok 'already stopped.'
fi
;;
restart)
if [ ${STATUS} -eq 0 ]
then
stop_service || die ${EC_RESTART_STOP_FAILED}
sleep 1
fi
start_service || die ${EC_RESTART_START_FAILED}
;;
configtest)
$1
;;
status)
status_of_proc "${DAEMON}" "${NAME}" || exit $?
;;
reload)
if [ ${STATUS} -eq 0 ]
then
configtest
if [[ $? -ne 0 ]]; then
die ${EC_RELOADING_FAILED}
fi
reload_service || die ${EC_RELOADING_FAILED}
else
fail 'service not running, nothing to be done.'
fi
;;
version)
$DAEMON version
;;
condrestart)
status_q && $0 restart || exit 0
;;
*)
# For invalid arguments, print the usage message.
echo "Usage: $0 {start|stop|restart|condrestart|reload|configtest|status|version}"
exit 2
;;
esac

8
misc/packaging/logrotate Normal file
View File

@@ -0,0 +1,8 @@
/var/log/centrifugo/centrifugo.log {
daily
rotate 7
missingok
dateext
copytruncate
compress
}

View File

@@ -0,0 +1,100 @@
#!/bin/bash
BIN_DIR=/usr/bin
DATA_DIR=/var/lib/centrifugo
LOG_DIR=/var/log/centrifugo
SCRIPT_DIR=/usr/lib/centrifugo/scripts
function install_init {
cp -f $SCRIPT_DIR/initd.sh /etc/init.d/centrifugo
chmod +x /etc/init.d/centrifugo
}
function install_initel6 {
cp -f $SCRIPT_DIR/initd.el6.sh /etc/init.d/centrifugo
chmod +x /etc/init.d/centrifugo
}
function install_systemd {
cp -f $SCRIPT_DIR/centrifugo.service /lib/systemd/system/centrifugo.service
systemctl is-enabled centrifugo &>/dev/null
if [[ $? -ne 0 ]]; then
systemctl enable centrifugo &>/dev/null
fi
systemctl daemon-reload
}
function install_upstart {
cp -f $SCRIPT_DIR/centrifugo.upstart /etc/init/centrifugo.conf
initctl reload-configuration
}
function install_update_rcd {
update-rc.d centrifugo defaults
}
function install_chkconfig {
chkconfig --add centrifugo
}
id centrifugo &>/dev/null
if [[ $? -ne 0 ]]; then
useradd --system -U -M centrifugo -s /bin/false -d $DATA_DIR
fi
if [ ! -d $DATA_DIR ]; then
mkdir -p $DATA_DIR
fi
if [ ! -d $LOG_DIR ]; then
mkdir -p $LOG_DIR
fi
chown -R -L centrifugo:centrifugo $DATA_DIR
chown -R -L centrifugo:centrifugo $LOG_DIR
# Add defaults file, if it doesn't exist
if [[ ! -f /etc/default/centrifugo ]]; then
touch /etc/default/centrifugo
fi
# Generate config with unique secret key, if it doesn't exist
if [[ ! -f /etc/centrifugo/config.json ]]; then
/usr/bin/centrifugo genconfig -c /etc/centrifugo/config.json
fi
# Distribution-specific logic
if [[ -f /etc/redhat-release ]]; then
# RHEL-variant logic
which systemctl &>/dev/null
if [[ $? -eq 0 ]]; then
install_systemd
else
# Assuming sysv
install_initel6
install_chkconfig
fi
elif [[ -f /etc/debian_version ]]; then
# Debian/Ubuntu logic
which systemctl &>/dev/null
if [[ $? -eq 0 ]]; then
install_systemd
else
/sbin/init --version >/dev/null 2>/dev/null
if [[ $? -eq 0 && `/sbin/init --version` =~ upstart ]]; then
# Assuming upstart
install_upstart
else
# Assuming sysv
install_init
install_update_rcd
fi
fi
elif [[ -f /etc/os-release ]]; then
source /etc/os-release
if [[ $ID = "amzn" ]]; then
# Amazon Linux logic
install_init
install_chkconfig
fi
fi

View File

@@ -0,0 +1,76 @@
#!/bin/bash
function disable_systemd {
systemctl is-active centrifugo &>/dev/null
if [[ $? -eq 0 ]]; then
systemctl stop centrifugo || :
fi
systemctl is-enabled centrifugo &>/dev/null
if [[ $? -ne 0 ]]; then
systemctl disable centrifugo &>/dev/null
fi
rm -f /lib/systemd/system/centrifugo.service
systemctl daemon-reload
}
function disable_upstart {
initctl stop centrifugo || :
rm -f /etc/init/centrifugo.conf
initctl reload-configuration
}
function disable_update_rcd {
service centrifugo stop || :
update-rc.d -f centrifugo remove
rm -f /etc/init.d/centrifugo
}
function disable_chkconfig {
service centrifugo stop || :
chkconfig --del centrifugo
rm -f /etc/init.d/centrifugo
}
if [[ -f /etc/redhat-release ]]; then
# RHEL-variant logic
if [[ "$1" = "0" ]]; then
# Centrifugo is no longer installed, remove from init system
which systemctl &>/dev/null
if [[ $? -eq 0 ]]; then
disable_systemd
else
# Assuming sysv
disable_chkconfig
fi
rm -f /etc/default/centrifugo
fi
elif [[ -f /etc/debian_version ]]; then
# Debian/Ubuntu logic
if [[ "$1" != "upgrade" ]]; then
# Remove/purge
which systemctl &>/dev/null
if [[ $? -eq 0 ]]; then
disable_systemd
else
/sbin/init --version >/dev/null 2>/dev/null
if [[ $? -eq 0 && `/sbin/init --version` =~ upstart ]]; then
# Assuming upstart
disable_upstart
else
# Assuming sysv
disable_update_rcd
fi
fi
rm -f /etc/default/centrifugo
fi
elif [[ -f /etc/os-release ]]; then
source /etc/os-release
if [[ $ID = "amzn" ]]; then
# Amazon Linux logic
if [[ "$1" = "0" ]]; then
# Centrifugo is no longer installed, remove from init system
disable_chkconfig
rm -f /etc/default/centrifugo
fi
fi
fi

View File

@@ -0,0 +1 @@
#!/bin/bash

View File

@@ -0,0 +1,3 @@
#!/bin/bash
echo "host replication all 0.0.0.0/0 trust" >> "$PGDATA/pg_hba.conf"
pg_ctl reload

View File

@@ -0,0 +1,102 @@
#!/bin/bash
# Function to wait for Redis node to become available
wait_for_redis() {
local port=$1
while ! redis-cli -p "$port" ping > /dev/null 2>&1; do
echo "Waiting for Redis node on port $port to start..."
sleep 1
done
echo "Redis node on port $port is available."
}
# Array to hold PIDs of Redis server processes
declare -a redis_pids
# Function to start Redis cluster nodes
start_redis_cluster() {
local masters=$1
local replicas=$2
local base_port=7001
local cluster_dir="./cluster_data"
local total_nodes=$((masters * (replicas + 1)))
local redis_nodes=()
# Create cluster directory, clean previous.
rm -r "$cluster_dir"
mkdir -p "$cluster_dir"
# Start master nodes
for ((i=0; i<masters; i++)); do
local port=$((base_port + i))
local node_dir="$cluster_dir/$port"
mkdir -p "$node_dir"
pushd "$node_dir" > /dev/null
# Start a Redis server with cluster enabled and no replicas
redis-server --port "$port" --cluster-enabled yes \
--cluster-config-file "nodes.conf" \
--cluster-node-timeout 5000 --save "" --appendonly no \
--appendfilename "appendonly.aof" \
--dbfilename "dump.rdb" \
--logfile "redis.log" &
redis_pids[i]=$!
popd > /dev/null
redis_nodes+=("127.0.0.1:$port")
done
# Start replica nodes
for ((i=0; i<masters * replicas; i++)); do
local port=$((base_port + masters + i))
local node_dir="$cluster_dir/$port"
mkdir -p "$node_dir"
pushd "$node_dir" > /dev/null
# Start a Redis server with cluster enabled and no replicas
redis-server --port "$port" --cluster-enabled yes \
--cluster-config-file "nodes.conf" \
--cluster-node-timeout 5000 --save "" --appendonly no \
--appendfilename "appendonly.aof" \
--dbfilename "dump.rdb" \
--logfile "redis.log" &
local pid=$!
redis_pids+=("$pid")
popd > /dev/null
redis_nodes+=("127.0.0.1:$port")
done
# Wait for all nodes to become available
for port in "${redis_nodes[@]}"; do
wait_for_redis "${port##*:}"
done
# Create the cluster with replicas
echo "Creating Redis cluster with the following nodes: ${redis_nodes[*]}"
yes "yes" | redis-cli --cluster create "${redis_nodes[@]}" --cluster-replicas "$replicas"
}
# Function to shut down Redis cluster nodes
shutdown_redis_cluster() {
echo "Shutting down Redis cluster nodes..."
for pid in "${redis_pids[@]}"; do
kill "$pid"
done
}
# Trap Ctrl+C (SIGINT) to call shutdown function
trap shutdown_redis_cluster SIGINT
# Check if the number of masters and replicas is provided
if [ -z "$1" ] || [ -z "$2" ]; then
echo "Usage: $0 <number_of_masters> <number_of_replicas_per_master>"
exit 1
fi
# Start the Redis cluster
start_redis_cluster "$1" "$2"
# Wait until a keyboard interrupt (Ctrl+C) is detected
echo "Redis cluster is running. Press Ctrl+C to stop."
wait
# The script will wait at the above line until the user presses Ctrl+C, which will trigger the shutdown function

View File

@@ -0,0 +1,9 @@
Create local Redis cluster consisting of N nodes with M replicas each, ex:
```
bash create_cluster.sh 3 0
```
Cluster nodes will work until script interrupted. The goal of having this script is for development and benchmarking.
All data will be saved to local directory `cluster_data`. By default, Redis runs without RDB and AOF (`--save "" --appendonly no`).

View File

@@ -0,0 +1,9 @@
Create local N isolated Redis master instances starting from port `6000`:
```
bash start_instances.sh 3
```
Redis nodes will work until script interrupted. The goal of having this script is for development and benchmarking.
By default, Redis runs without RDB and AOF (`--save "" --appendonly no`).

View File

@@ -0,0 +1,75 @@
#!/bin/bash
# Function to wait for Redis node to become available
wait_for_redis() {
local port=$1
while ! redis-cli -p "$port" ping > /dev/null 2>&1; do
echo "Waiting for Redis node on port $port to start..."
sleep 1
done
echo "Redis node on port $port is available."
}
# Array to hold PIDs of Redis server processes
declare -a redis_pids
# Function to start Redis nodes
start_redis_instances() {
local masters=$1
local base_port=6000
local redis_dir="./redis_data"
local total_nodes=$((masters * (0 + 1)))
local redis_nodes=()
# Create cluster directory, clean previous.
rm -r "$redis_dir"
mkdir -p "$redis_dir"
# Start master nodes
for ((i=0; i<masters; i++)); do
local port=$((base_port + i))
local node_dir="$redis_dir/$port"
mkdir -p "$node_dir"
pushd "$node_dir" > /dev/null
redis-server --port "$port" \
--save "" --appendonly no \
--appendfilename "appendonly.aof" \
--dbfilename "dump.rdb" \
--logfile "redis.log" &
redis_pids[i]=$!
popd > /dev/null
redis_nodes+=("127.0.0.1:$port")
done
# Wait for all nodes to become available
for port in "${redis_nodes[@]}"; do
wait_for_redis "${port##*:}"
done
}
# Function to shut down Redis nodes
shutdown_redis_instances() {
echo "Shutting down Redis nodes..."
for pid in "${redis_pids[@]}"; do
kill "$pid"
done
}
# Trap Ctrl+C (SIGINT) to call shutdown function
trap shutdown_redis_instances SIGINT
# Check if the number of masters and replicas is provided
if [ -z "$1" ]; then
echo "Usage: $0 <number_of_masters>"
exit 1
fi
# Start Redis instances
start_redis_instances "$1"
# Wait until a keyboard interrupt (Ctrl+C) is detected
echo "Redis instances are running. Press Ctrl+C to stop."
wait
# The script will wait at the above line until the user presses Ctrl+C, which will trigger the shutdown function

View File

@@ -0,0 +1,21 @@
Create local Sentinel based Redis setup consisting of 1 Sentinel and 3 Redis nodes (master, 2 replicas). Note, in production you need at least 3 Sentinels for high availability.
## Run
```
bash start.sh
```
## Emulate failover
Connect to Sentinel:
```
redis-cli -p 26379
```
And run:
```
sentinel failover mymaster
```

View File

@@ -0,0 +1,94 @@
#!/bin/bash
# Function to wait for Redis node to become available
wait_for_redis() {
local port=$1
while ! redis-cli -p "$port" ping > /dev/null 2>&1; do
echo "Waiting for Redis node on port $port to start..."
sleep 1
done
echo "Redis node on port $port is available."
}
# Array to hold PIDs of Redis server processes
declare -a redis_pids
# Function to start Redis Sentinel setup
start_redis_sentinel() {
local base_port=7000
local sentinel_port=26379
local data_dir="./sentinel_data"
# Clean up previous setup
rm -rf "$data_dir"
mkdir -p "$data_dir"
# Start the master node
local master_port=$((base_port))
local master_dir="$data_dir/master"
mkdir -p "$master_dir"
pushd "$master_dir" > /dev/null
redis-server --port "$master_port" --save "" --appendonly no --dbfilename "dump.rdb" --logfile "redis.log" &
local pid=$!
redis_pids+=($pid)
echo "Master started on port $master_port with PID $pid"
popd > /dev/null
# Start the replicas
for i in 1 2; do
local replica_port=$((base_port + i))
local replica_dir="$data_dir/replica_$i"
mkdir -p "$replica_dir"
pushd "$replica_dir" > /dev/null
redis-server --port "$replica_port" --slaveof 127.0.0.1 "$master_port" --save "" --appendonly no --dbfilename "dump.rdb" --logfile "redis.log" &
pid=$!
redis_pids+=($pid)
echo "Replica $i started on port $replica_port with PID $pid"
popd > /dev/null
done
# Wait for all nodes to become available
wait_for_redis "$master_port"
for i in 1 2; do
wait_for_redis $((base_port + i))
done
# Start Redis Sentinel
local sentinel_dir="$data_dir/sentinel"
mkdir -p "$sentinel_dir"
pushd "$sentinel_dir" > /dev/null
cat <<EOF > sentinel.conf
port $sentinel_port
sentinel monitor mymaster 127.0.0.1 $master_port 1
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
EOF
redis-server sentinel.conf --sentinel &
pid=$!
redis_pids+=($pid)
echo "Sentinel started on port $sentinel_port with PID $pid"
popd > /dev/null
}
# Function to shut down Redis nodes and Sentinel
shutdown_redis_sentinel() {
echo "Shutting down Redis nodes and Sentinel..."
for pid in "${redis_pids[@]}"; do
kill "$pid"
echo "Stopped process with PID $pid"
done
}
# Trap Ctrl+C (SIGINT) to call shutdown function
trap shutdown_redis_sentinel SIGINT
# Start the Redis Sentinel setup
start_redis_sentinel
# Wait until a keyboard interrupt (Ctrl+C) is detected
echo "Redis Sentinel setup is running. Press Ctrl+C to stop."
echo "All PIDs: ${redis_pids[*]}"
wait

21
misc/release/notes.md Normal file
View File

@@ -0,0 +1,21 @@
Centrifugo is an open-source scalable real-time messaging server. It instantly delivers messages to application online users connected over supported transports (WebSocket, HTTP-streaming, Server-Sent Events (SSE), GRPC, WebTransport). Centrifugo is built around channel subscriptions – so it's a user-facing PUB/SUB server.
Centrifugo is language-agnostic and can be used to build chat apps, live comments, multiplayer games, real-time data visualizations, collaborative tools, AI streaming responses, etc. in combination with any backend. It is well suited for modern architectures and allows decoupling the business logic from the real-time transport layer.
Official client SDKs are available for JavaScript (browser, Node.js, React Native), Dart/Flutter, Swift, Java, Python, Go, and .NET. In addition, Centrifugo supports a unidirectional approach for simple use cases with no SDK dependency.
For details, go to the [Centrifugo documentation site](https://centrifugal.dev). For runnable demos see [centrifugal/examples](https://github.com/centrifugal/examples).
## What's changed
### Fixes
* A client-side subscription refresh with an already expired subscription token was accepted. The subscription's expiration time was then cleared, so the subscription never expired. Now Centrifugo closes the connection with the `3006` (`subscription expired`) disconnect code. The client reconnects, gets a token expired error on resubscribe and requests a new token – all official SDKs already handle this ([centrifugal/centrifuge#627](https://github.com/centrifugal/centrifuge/pull/627)).
* Fossil delta compression with recovery: when a subscribe with recovery found no missed publications, the next publication was still sent as a delta. The client had no base data for it, so it could not decode that publication or any after it. Now the first publication after such a subscribe is sent with full data ([centrifugal/centrifuge#629](https://github.com/centrifugal/centrifuge/pull/629)).
* Align fossil deltas to UTF-8 character boundaries for JSON clients [centrifugal/centrifuge#630](https://github.com/centrifugal/centrifuge/pull/630). For a change inside a multi-byte character it copies the first bytes of the character from the previous data and inserts the rest. The inserted bytes aren't valid UTF-8, json.Escape replaces them with U+FFFD, and the client fails to apply the delta. The fix eliminates this.
### Miscellaneous
* This release is built with Go 1.26.8.
* Dependency updates.
* See also the corresponding [Centrifugo PRO release](https://github.com/centrifugal/centrifugo-pro/releases/tag/v6.9.6).

21
misc/rpm/README.md Normal file
View File

@@ -0,0 +1,21 @@
RPM for Centrifugo
```
yum -y install rpmdevtools
```
Go to directory containing all files listed here.
Download release to current folder:
```
wget https://github.com/centrifugal/centrifugo/releases/download/v0.0.1/centrifugo-0.0.1-linux-amd64.zip -O centrifugo-0.0.1-linux-amd64.zip
```
Build rpm:
```
./build.sh 0.0.1
```
As argument to `build.sh` you should specify Centrifugo version of release binary downloaded above.

20
misc/rpm/build.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
if [ "$1" = "" ]
then
echo "Usage: $0 <version>"
exit 1
fi
if [ ! -f centrifugo-$1-linux-amd64.zip ]; then
echo "No Centrifugo release found in current directory"
exit 1
fi
cp centrifugo-$1-linux-amd64.zip ~/rpmbuild/SOURCES/centrifugo-$1-linux-amd64.zip
cp centrifugo.spec ~/rpmbuild/SPECS/centrifugo.spec
cp centrifugo.initd ~/rpmbuild/SOURCES/centrifugo.initd
cp centrifugo.nofiles.conf ~/rpmbuild/SOURCES/centrifugo.nofiles.conf
cp centrifugo.logrotate ~/rpmbuild/SOURCES/centrifugo.logrotate
cp centrifugo.config.json ~/rpmbuild/SOURCES/centrifugo.config.json
rpmbuild -bb ~/rpmbuild/SPECS/centrifugo.spec --define "version $1" --define "release `date +%s`"

View File

@@ -0,0 +1 @@
{}

148
misc/rpm/centrifugo.initd Normal file
View File

@@ -0,0 +1,148 @@
#!/bin/bash
#
# /etc/init.d/centrifugo
#
# Startup script for centrifugo
#
# chkconfig: 2345 20 80
# description: Starts and stops centrifugo
# Source function library.
if [ -f /etc/init.d/functions ] ; then
. /etc/init.d/functions
elif [ -f /etc/rc.d/init.d/functions ] ; then
. /etc/rc.d/init.d/functions
else
exit 1
fi
# Daemon options
CENTRIFUGO_OPTS=
# Process name
NAME=centrifugo
# User and group
USER=centrifugo
GROUP=centrifugo
# Daemon name, where is the actual executable
# If the daemon is not there, then exit.
DAEMON=/usr/sbin/centrifugo
# Configuration file
CONFIG=/etc/centrifugo/config.json
# Logging
if [ -z "$STDOUT" ]; then
STDOUT=/var/log/centrifugo/centrifugo.log
fi
if [ ! -f "$STDOUT" ]; then
mkdir -p $(dirname $STDOUT)
fi
if [ -z "$STDERR" ]; then
STDERR=/var/log/centrifugo/centrifugo.log
fi
if [ ! -f "$STDERR" ]; then
mkdir -p $(dirname $STDERR)
fi
[ -e /etc/sysconfig/centrifugo ] && . /etc/sysconfig/centrifugo
lockfile="/var/lock/subsys/centrifugo"
if ! [ -x $DAEMON ]; then
echo "$DAEMON binary not found."
exit 5
fi
start() {
if ! [ -f $CONFIG ]; then
echo "configuration file not found $CONFIG"
exit 6
fi
configtest || return $?
echo -n $"Starting $NAME: "
daemon --user centrifugo --check=$DAEMON "nohup $DAEMON --config=$CONFIG $CENTRIFUGO_OPTS >>$STDOUT 2>&1 &"
retval=$?
echo
[ $retval -eq 0 ] && touch $lockfile
return $retval
}
stop() {
echo -n $"Stopping $NAME: "
killproc $NAME -TERM
retval=$?
if [ $retval -eq 0 ]; then
if [ "$CONSOLETYPE" != "serial" ]; then
echo -en "\\033[16G"
fi
while rh_status_q
do
sleep 1
echo -n $"."
done
rm -f $lockfile
fi
echo
return $retval
}
restart() {
configtest || return $?
stop
sleep 1
start
}
reload() {
configtest || return $?
echo -n $"Reloading $NAME: "
killproc $DAEMON -HUP
sleep 1
RETVAL=$?
echo
}
configtest() {
$DAEMON checkconfig -c $CONFIG
}
rh_status() {
status $NAME
}
rh_status_q() {
rh_status >/dev/null 2>&1
}
case "$1" in
start)
rh_status_q && exit 0
$1
;;
stop)
rh_status_q || exit 0
$1
;;
restart|configtest)
$1
;;
reload)
rh_status_q || exit 7
$1
;;
status|status_q)
rh_$1
;;
condrestart)
rh_status_q && restart || exit 0
;;
*)
echo $"Usage: $0 {start|stop|reload|status|restart|configtest|condrestart}"
exit 2
esac

View File

@@ -0,0 +1,7 @@
/var/log/centrifugo/*log {
copytruncate
daily
rotate 7
compress
missingok
}

View File

@@ -0,0 +1 @@
centrifugo - nofile 65536

92
misc/rpm/centrifugo.spec Normal file
View File

@@ -0,0 +1,92 @@
%define debug_package %{nil}
%define application_user %{name}
%define application_group %{name}
%define application_data %{_localstatedir}/lib/%{name}
%define __prefix /opt
Name: centrifugo
Version: %{version}
Release: %{release}
Summary: Real-time messaging server
License: MIT
URL: https://github.com/centrifugal/centrifugo
Group: Apps/sys
Buildroot: %{_tmppath}/%{name}-%{version}-%{release}-%(%{__id_u} -n)
Packager: Alexandr Emelin <frvzmb@gmail.com>
Requires(pre): shadow-utils
Requires(post): /sbin/chkconfig
Requires(preun): /sbin/chkconfig, /sbin/service
Requires(postun): /sbin/service
%description
Real-time messaging server written in Go
%prep
cd ~/rpmbuild/BUILD
rm -rf %{name}-%{version}-linux-amd64
unzip ~/rpmbuild/SOURCES/%{name}-%{version}-linux-amd64.zip
if [ $? -ne 0 ]; then
exit $?
fi
ls
cd %{name}-%{version}-linux-amd64
chown -R $USER.$USER .
chmod -R a+rX,g-w,o-w .
%build
rm -rf %{buildroot}
echo %{buildroot}
%install
install -d -m 755 %{buildroot}/%{_sbindir}
install -m 755 %{_builddir}/%{name}-%{version}-linux-amd64/centrifugo %{buildroot}/%{_sbindir}
install -d -m 755 %{buildroot}%{__prefix}/%{name}
install -d -m 755 %{buildroot}/%{_localstatedir}/log/%{name}
install -d -m 755 %{buildroot}/%{_localstatedir}/lib/%{name}
install -d -m 755 %{buildroot}/%{_initrddir}
install -m 755 %_sourcedir/%{name}.initd %{buildroot}/%{_initrddir}/%{name}
install -d -m 755 %{buildroot}/%{_sysconfdir}/security/limits.d/
install -m 644 %_sourcedir/%{name}.nofiles.conf %{buildroot}/%{_sysconfdir}/security/limits.d/%{name}.nofiles.conf
install -d -m 755 %{buildroot}/%{_sysconfdir}/logrotate.d
install -m 644 %_sourcedir/%{name}.logrotate %{buildroot}/%{_sysconfdir}/logrotate.d/%{name}
install -d -m 755 %{buildroot}/%{_sysconfdir}/%{name}/
install -m 644 %_sourcedir/%{name}.config.json %{buildroot}/%{_sysconfdir}/%{name}/config.json
%clean
rm -rf %{buildroot}
%pre
getent group %{application_group} >/dev/null || groupadd -r %{application_group}
getent passwd %{application_user} >/dev/null || /usr/sbin/useradd --comment "centrifugo Daemon User" --shell /bin/bash -M -r -g %{application_group} --home %{application_data} %{application_user}
%post
chkconfig --add %{name}
%preun
if [ $1 = 0 ]; then
service %{name} stop > /dev/null 2>&1 || :
chkconfig --del %{name}
fi
%postun
if [ $1 == 1 ] ; then
service %{name} condrestart >/dev/null 2>&1 || :
fi
%files
%defattr(-,root,root)
%{_sbindir}/%{name}
%attr(0755,%{application_user},%{application_group}) %dir %{_localstatedir}/log/%{name}
%attr(0755,%{application_user},%{application_group}) %dir %{_localstatedir}/lib/%{name}
%{_initrddir}/%{name}
%{__prefix}/%{name}
%config(noreplace) %{_sysconfdir}/%{name}/config.json
%config(noreplace) %{_sysconfdir}/security/limits.d/%{name}.nofiles.conf
%config(noreplace) %{_sysconfdir}/logrotate.d/%{name}

17
misc/scripts/generate.sh Normal file
View File

@@ -0,0 +1,17 @@
set -e
cd internal/apiproto
bash generate.sh
cd -
cd internal/apiproto/swagger
bash generate_swagger.sh
cd -
cd internal/proxyproto
bash generate.sh
cd -
cd internal/unigrpc/unistream
bash generate.sh
cd -

207
misc/scripts/package.sh Executable file
View File

@@ -0,0 +1,207 @@
#!/bin/sh
#
# Debian 7: lsb sysv
# Debian 8: systemd
# Ubuntu 14.04: upstart
# Ubuntu 16.04: systemd
# Centos 6: sysv
# Centos 7: systemd
#
if [ "$1" = "" ]
then
echo "Usage: $0 <version> <iteration>"
exit
fi
if [ "$2" = "" ]
then
echo "Usage: $0 <version> <iteration>"
exit
fi
VERSION=$1
ITERATION=$2
UNIX_NOW=$( date +%s )
INSTALL_DIR=/usr/bin
LOG_DIR=/var/log/centrifugo
CONFIG_DIR=/etc/centrifugo
LOGROTATE_DIR=/etc/logrotate.d
DATA_DIR=/var/lib/centrifugo
SCRIPT_DIR=/usr/lib/centrifugo
SAMPLE_CONFIGURATION=misc/packaging/config.json
INITD_SCRIPT=misc/packaging/initd.sh
INITD_EL6_SCRIPT=misc/packaging/initd.el6.sh
UPSTART_SCRIPT=misc/packaging/centrifugo.upstart
SYSTEMD_SCRIPT=misc/packaging/centrifugo.service
POSTINSTALL_SCRIPT=misc/packaging/post_install.sh
PREINSTALL_SCRIPT=misc/packaging/pre_install.sh
POSTUNINSTALL_SCRIPT=misc/packaging/post_uninstall.sh
LOGROTATE=misc/packaging/logrotate
TMP_WORK_DIR=`mktemp -d`
TMP_BINARIES_DIR=`mktemp -d`
ARCH=amd64
NAME=centrifugo
LICENSE=MIT
URL="https://github.com/centrifugal/centrifugo"
MAINTAINER="frvzmb@gmail.com"
VENDOR=centrifugo
DESCRIPTION="Real-time messaging server"
echo "Start packaging, version: $VERSION, iteration: $ITERATION, release time: $UNIX_NOW"
# cleanup_exit removes all resources created during the process and exits with
# the supplied returned code.
cleanup_exit() {
rm -r $TMP_WORK_DIR
rm -r $TMP_BINARIES_DIR
exit $1
}
# make_dir_tree creates the directory structure within the packages.
make_dir_tree() {
work_dir=$1
mkdir -p $work_dir/$INSTALL_DIR
if [ $? -ne 0 ]; then
echo "Failed to create install directory -- aborting."
cleanup_exit 1
fi
mkdir -p $work_dir/$SCRIPT_DIR/scripts
if [ $? -ne 0 ]; then
echo "Failed to create script directory -- aborting."
cleanup_exit 1
fi
mkdir -p $work_dir/$CONFIG_DIR
if [ $? -ne 0 ]; then
echo "Failed to create configuration directory -- aborting."
cleanup_exit 1
fi
mkdir -p $work_dir/$LOGROTATE_DIR
if [ $? -ne 0 ]; then
echo "Failed to create logrotate directory -- aborting."
cleanup_exit 1
fi
}
# do_build builds the code. The version and commit must be passed in.
do_build() {
echo "Start building binary"
if [ -z "$STATS_ENDPOINT" ]
then
echo "STATS_ENDPOINT not defined"
cleanup_exit 1
else
echo "STATS_ENDPOINT found"
fi
if [ -z "$STATS_TOKEN" ]
then
echo "STATS_TOKEN not defined"
cleanup_exit 1
else
echo "STATS_TOKEN found"
fi
CGO_ENABLED=0 GOOS="linux" GOARCH="amd64" go build -ldflags="-X github.com/centrifugal/centrifugo/v6/internal/build.Version=$VERSION -X github.com/centrifugal/centrifugo/v6/internal/build.UsageStatsEndpoint=$STATS_ENDPOINT -X github.com/centrifugal/centrifugo/v6/internal/build.UsageStatsToken=$STATS_TOKEN" -o "$TMP_BINARIES_DIR/centrifugo"
echo "Binary build completed successfully"
"$TMP_BINARIES_DIR"/centrifugo version
}
do_build $VERSION
make_dir_tree $TMP_WORK_DIR
cp $TMP_BINARIES_DIR/centrifugo $TMP_WORK_DIR/$INSTALL_DIR/
if [ $? -ne 0 ]; then
echo "Failed to copy binaries to packaging directory ($TMP_WORK_DIR/$INSTALL_DIR/) -- aborting."
cleanup_exit 1
fi
echo "centrifugo binary copied to $TMP_WORK_DIR/$INSTALL_DIR/"
cp $INITD_SCRIPT $TMP_WORK_DIR/$SCRIPT_DIR/scripts/initd.sh
if [ $? -ne 0 ]; then
echo "Failed to copy init.d script to packaging directory ($TMP_WORK_DIR/$SCRIPT_DIR/) -- aborting."
cleanup_exit 1
fi
echo "initd script copied to $TMP_WORK_DIR/$SCRIPT_DIR/scripts"
cp $INITD_EL6_SCRIPT $TMP_WORK_DIR/$SCRIPT_DIR/scripts/initd.el6.sh
if [ $? -ne 0 ]; then
echo "Failed to copy initd.el6.sh script to packaging directory ($TMP_WORK_DIR/$SCRIPT_DIR/) -- aborting."
cleanup_exit 1
fi
echo "initd.el6 script copied to $TMP_WORK_DIR/$SCRIPT_DIR/scripts"
cp $SYSTEMD_SCRIPT $TMP_WORK_DIR/$SCRIPT_DIR/scripts/centrifugo.service
if [ $? -ne 0 ]; then
echo "Failed to copy systemd script to packaging directory -- aborting."
cleanup_exit 1
fi
echo "systemd script copied to $TMP_WORK_DIR/$SCRIPT_DIR/scripts"
cp $UPSTART_SCRIPT $TMP_WORK_DIR/$SCRIPT_DIR/scripts/centrifugo.upstart
if [ $? -ne 0 ]; then
echo "Failed to copy upstart script to packaging directory -- aborting."
cleanup_exit 1
fi
echo "upstart script copied to $TMP_WORK_DIR/$SCRIPT_DIR/scripts"
install -m 644 $LOGROTATE $TMP_WORK_DIR/$LOGROTATE_DIR/centrifugo
if [ $? -ne 0 ]; then
echo "Failed to copy logrotate configuration to packaging directory -- aborting."
cleanup_exit 1
fi
echo "logrotate script copied to $TMP_WORK_DIR/$LOGROTATE_DIR/centrifugo"
COMMON_FPM_ARGS="\
-C $TMP_WORK_DIR \
--log error \
--version $VERSION \
--iteration $ITERATION \
--name $NAME \
--vendor $VENDOR \
--url $URL \
--category Network \
--license $LICENSE \
--maintainer $MAINTAINER \
--force \
--after-install $POSTINSTALL_SCRIPT \
--before-install $PREINSTALL_SCRIPT \
--after-remove $POSTUNINSTALL_SCRIPT \
--config-files $CONFIG_DIR \
--config-files $LOGROTATE_DIR "
echo "Start building rpm package"
rm -r ./PACKAGES
mkdir -p PACKAGES
fpm -s dir -t rpm $COMMON_FPM_ARGS --description "$DESCRIPTION" \
--rpm-os linux \
-p PACKAGES/ \
-a amd64 .
echo "Start building deb package"
fpm -s dir -t deb $COMMON_FPM_ARGS --description "$DESCRIPTION" \
-p PACKAGES/ \
-a amd64 .
cd PACKAGES
for f in *.deb; do sha256sum $f >> ${f}_checksum.txt; done
for f in *.rpm; do sha256sum $f >> ${f}_checksum.txt; done
cd ..
echo "Packaging complete!"
cleanup_exit 0

View File

@@ -0,0 +1,6 @@
#!/bin/sh
sudo gem install rake
sudo gem install package_cloud
sudo gem install fpm
sudo apt-get install -y rpm
make package

View File

@@ -0,0 +1,2 @@
#!/bin/sh
make packagecloud

56
misc/scripts/update-deps.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Update all Go module dependencies.
#
# Usage:
# ./scripts/update-deps.sh # minor/patch updates only
# ./scripts/update-deps.sh --major # also apply major version updates
set -euo pipefail
cd "$(pwd)"
pwd
# Packages to exclude from automatic updates (updated manually).
EXCLUDE=(
"github.com/centrifugal/centrifuge"
"github.com/mailru/easyjson"
)
# Record current versions of excluded packages.
SAVED=()
for pkg in "${EXCLUDE[@]}"; do
ver=$(grep "^[[:space:]]*${pkg} " go.mod | awk '{print $2}')
if [[ -n "$ver" ]]; then
SAVED+=("${pkg}@${ver}")
fi
done
echo "==> Updating all dependencies to latest minor/patch versions..."
go get -u ./...
# Restore excluded packages to their original versions.
for entry in "${SAVED[@]}"; do
echo "==> Pinning $entry (excluded from update)"
go get "$entry"
done
echo "==> Running go mod tidy..."
go mod tidy
if [[ "${1:-}" == "--major" ]]; then
if ! command -v gomajor &>/dev/null; then
echo "gomajor not found, installing..."
go install github.com/icholy/gomajor@latest
fi
echo "==> Checking for major version updates..."
gomajor list
echo ""
read -rp "Apply all major version updates? [y/N] " answer
if [[ "$answer" =~ ^[Yy]$ ]]; then
gomajor get -a ./...
echo "==> Running go mod tidy after major updates..."
go mod tidy
fi
fi
echo "==> Done. Review changes with: git diff go.mod"

View File

@@ -0,0 +1,25 @@
#!/bin/bash
# this script updates embedded swagger web interface.
# Script intended to be run from the repo root folder:
# ./misc/scripts/update_swagger_web.sh
TMP_WORK_DIR=$(mktemp -d)
# cleanup_exit removes all resources created during the process and exits with
# the supplied returned code.
cleanup_exit() {
rm -rf "$TMP_WORK_DIR"
exit "$1"
}
git clone -c advice.detachedHead=false --depth 1 --branch v4.18.1 --single-branch https://github.com/swagger-api/swagger-ui.git "$TMP_WORK_DIR"
cp internal/apiproto/swagger/api.swagger.json "$TMP_WORK_DIR"/dist/swagger.json
oldString="https://petstore.swagger.io/v2/"
newString="./"
sed -i '' "s|${oldString}|${newString}|g" "$TMP_WORK_DIR"/dist/swagger-initializer.js
statik -src="$TMP_WORK_DIR"/dist -dest ./internal/ -package=swaggerui
cleanup_exit 0

20
misc/scripts/update_web.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
# this script updates embedded web interface.
# Script intended to be run from the repo root folder:
# ./misc/scripts/update_web.sh
TMP_WORK_DIR=$(mktemp -d)
# cleanup_exit removes all resources created during the process and exits with
# the supplied returned code.
cleanup_exit() {
rm -rf "$TMP_WORK_DIR"
exit "$1"
}
git clone -c advice.detachedHead=false --depth 1 --branch main --single-branch https://github.com/centrifugal/web.git "$TMP_WORK_DIR"
statik -src="$TMP_WORK_DIR"/dist -dest ./internal/ -package=webui
cleanup_exit 0

7
misc/wrk/api_history.lua Normal file
View File

@@ -0,0 +1,7 @@
-- CENTRIFUGO_HISTORY_SIZE=100 CENTRIFUGO_HISTORY_TTL=3600s ./centrifugo --api_insecure
-- for i in {1..100}; do curl -X POST http://localhost:8000/api --data '{"method": "publish", "params": {"channel": "index", "data": "hello"}}'; done
-- curl -X POST http://localhost:8000/api --data '{"method": "history", "params": {"channel": "index", "limit": -1}}' | python3 -m json.tool
-- wrk -s api_history.lua http://localhost:8000/api -d 2s -t 5 -c 40
wrk.method = "POST"
wrk.body = '{"method": "history", "params": {"channel": "index", "limit": -1}}'
wrk.headers["Content-Type"] = "application/json"

5
misc/wrk/api_publish.lua Normal file
View File

@@ -0,0 +1,5 @@
-- ./centrifugo --api_insecure
-- wrk -s api_publish.lua http://localhost:8000/api -d 2s -t 5 -c 40
wrk.method = "POST"
wrk.body = '{"method": "publish", "params": {"channel": "index", "data": 1}}'
wrk.headers["Content-Type"] = "application/json"