Compare commits

..

8 Commits

Author SHA1 Message Date
efim
ea85460c0a fix: remove all marks when noone speaks next
if room comes to a state where last person stopped speaking,
then setting room to clean state is logical,
next person to start speaking is the only reference point
2023-12-03 14:39:40 +00:00
efim
acd9f4fc62 feat: custom gesture & room metrics 2023-12-02 11:30:56 +00:00
efim
42c73c5902 feat: prometheus nixos module & test-container
for testing grafana in container
export QEMU_NET_OPTS="hostfwd=tcp::3000-:3000"
 nixos-rebuild build-vm --flake .#test-container
and then run the container, login is test/test

and the grafana port is forwarded to host machine, yay
2023-12-02 08:19:13 +00:00
efim
0a8db09fe8 feat: reporting basic metrics to prometheus 2023-12-02 06:54:11 +00:00
efim
5cab5d88d9 feat: simplest room expiration 2023-12-01 04:14:23 +00:00
efim
a502ee72a0 fix: request context to rooms manager 2023-12-01 04:13:10 +00:00
efim
cc33c3f742 feat: expire & prolong sessions 2023-12-01 04:09:17 +00:00
efim
e0bd77fe3b fix: page names 2023-11-30 04:17:54 +00:00
16 changed files with 1436 additions and 50 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@
/result /result
/routes/static/out.css /routes/static/out.css
/nixos.qcow2 /nixos.qcow2
/data/

File diff suppressed because it is too large Load Diff

7
dev-conf/prometheus.yml Normal file
View File

@@ -0,0 +1,7 @@
global:
scrape_interval: 15s
scrape_configs:
- job_name: go
static_configs:
- targets: ['localhost:8081']

View File

@@ -22,17 +22,20 @@
pkgs.nodePackages.prettier pkgs.nodePackages.prettier
pkgs.gnumake pkgs.gnumake
pkgs.redis pkgs.redis
pkgs.prometheus
pkgs.grafana
]; ];
shellHook = '' shellHook = ''
export GOPATH=$PWD/.go export GOPATH=$PWD/.go
export PATH=$GOPATH/bin:$PATH export PATH=$GOPATH/bin:$PATH
export GRAPHANA_PATH=${pkgs.grafana}
''; '';
}; };
packages = rec { packages = rec {
some-automoderation = pkgs.buildGoModule { some-automoderation = pkgs.buildGoModule {
inherit pname version; inherit pname version;
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.; src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
vendorHash = "sha256-zc4n5UxsmW8Nt52kS57i1W61Gy/J8T0RJPlwJnYJjHI="; vendorHash = "sha256-ID0WG0pa9DkXTJ7aB9VywGO3R85FWkpXaaIuugnG6mg=";
preBuild = '' preBuild = ''
${pkgs.nodePackages.tailwindcss}/bin/tailwindcss -i routes/in.css -o routes/static/out.css ${pkgs.nodePackages.tailwindcss}/bin/tailwindcss -i routes/in.css -o routes/static/out.css
@@ -54,7 +57,7 @@
''; '';
}; };
networking.firewall.enable = false; networking.firewall.enable = false;
users.groups.test = {}; users.groups.test = { };
users.mutableUsers = false; users.mutableUsers = false;
users.users.test = { users.users.test = {
isNormalUser = true; isNormalUser = true;
@@ -67,15 +70,33 @@
host = "some-automoderation.sunshine.industries"; host = "some-automoderation.sunshine.industries";
useNginx = false; useNginx = false;
port = 9090; port = 9090;
metricsPort = 9091;
redisPort = 9999; redisPort = 9999;
enablePrometheus = true;
};
services.prometheus = {
enable = true;
port = 9998;
# scrape config will be set up by the module
};
services.grafana = {
enable = true;
settings.server.http_port = 3000;
settings.server.http_addr = "0.0.0.0";
provision.datasources = {
settings.datasources = [{
name = "local-prometheus";
type = "prometheus";
url = "http://localhost:9998";
}];
};
}; };
}) })
]; ];
}; };
}; };
nixosModules.some-automoderation-module = { lib, pkgs, config, ... }: nixosModules.some-automoderation-module = { lib, pkgs, config, ... }:
let let cfg = config.services.${pname};
cfg = config.services.${pname};
in { in {
options.services.${pname} = { options.services.${pname} = {
enable = enable =
@@ -88,8 +109,7 @@
useNginx = lib.mkOption { useNginx = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = true; default = true;
description = description = "Whether to set up nginx reverse proxy";
"Whether to set up nginx reverse proxy";
}; };
port = lib.mkOption { port = lib.mkOption {
type = lib.types.int; type = lib.types.int;
@@ -102,6 +122,16 @@
default = 7777; default = 7777;
description = "Port on which to connect to redis database."; description = "Port on which to connect to redis database.";
}; };
metricsPort = lib.mkOption {
type = lib.types.int;
default = 8091;
description = "Port on which server exposes metrics.";
};
enablePrometheus = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to add scrape rules to prometheus";
};
useHostTls = lib.mkOption { useHostTls = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
@@ -109,12 +139,10 @@
"Whether virtual host should enable NixOS ACME certs"; "Whether virtual host should enable NixOS ACME certs";
}; };
}; };
config = config = let
let
username = "${pname}"; username = "${pname}";
groupname = "${pname}"; groupname = "${pname}";
in in lib.mkIf cfg.enable {
lib.mkIf cfg.enable {
users.groups."${groupname}" = { }; users.groups."${groupname}" = { };
users.users."${username}" = { users.users."${username}" = {
isNormalUser = true; # needed to allow for home dir isNormalUser = true; # needed to allow for home dir
@@ -128,8 +156,9 @@
startLimitBurst = 10; startLimitBurst = 10;
serviceConfig = { serviceConfig = {
ExecStart = let ExecStart = let
serveCliArg = serveCliArg = "--port ${toString cfg.port} --redisPort ${
"--port ${toString cfg.port} --redisPort ${toString cfg.redisPort}"; toString cfg.redisPort
} --metricsPort ${toString cfg.metricsPort}";
in "${packages.some-automoderation}/bin/${pname} ${serveCliArg}"; in "${packages.some-automoderation}/bin/${pname} ${serveCliArg}";
Restart = "on-failure"; Restart = "on-failure";
User = "${username}"; User = "${username}";
@@ -154,10 +183,15 @@
enable = true; enable = true;
user = "${username}"; user = "${username}";
port = cfg.redisPort; port = cfg.redisPort;
settings = { settings = { notify-keyspace-events = "KEA"; };
notify-keyspace-events = "KEA"; };
} services.prometheus = lib.mkIf cfg.enablePrometheus {
; scrapeConfigs = [{
job_name = "some-automoderation";
static_configs = [{
targets = [ "localhost:${toString cfg.metricsPort}" ];
}];
}];
}; };
}; };
}; };

11
go.mod
View File

@@ -4,13 +4,22 @@ go 1.20
require ( require (
github.com/kr/pretty v0.3.1 github.com/kr/pretty v0.3.1
github.com/prometheus/client_golang v1.17.0
github.com/redis/go-redis/v9 v9.2.1 github.com/redis/go-redis/v9 v9.2.1
golang.org/x/crypto v0.15.0 golang.org/x/crypto v0.15.0
) )
require ( require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
golang.org/x/sys v0.14.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
) )

30
go.sum
View File

@@ -1,18 +1,46 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg= github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

27
main.go
View File

@@ -7,8 +7,10 @@ import (
"log" "log"
"net/http" "net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"sunshine.industries/some-automoderation/metrics"
"sunshine.industries/some-automoderation/rooms" "sunshine.industries/some-automoderation/rooms"
"sunshine.industries/some-automoderation/routes" "sunshine.industries/some-automoderation/routes"
"sunshine.industries/some-automoderation/sessions" "sunshine.industries/some-automoderation/sessions"
@@ -19,21 +21,34 @@ var ctx = context.Background()
func main() { func main() {
var port int var port int
flag.IntVar(&port, "port", 8080, "Port on which the server should start") flag.IntVar(&port, "port", 8080, "Port on which the server should start")
var metricsPort int
flag.IntVar(&metricsPort, "metricsPort", 8081, "Port on which the server expose metrics")
var redisPort int var redisPort int
flag.IntVar(&redisPort, "redisPort", 7777, "Port on which server should connect to redis db") flag.IntVar(&redisPort, "redisPort", 7777, "Port on which server should connect to redis db")
flag.Parse() flag.Parse()
metrics := metrics.SetupMetrics()
promhttp.Handler()
promHandler := promhttp.HandlerFor(metrics.Registry, promhttp.HandlerOpts{})
promMux := http.NewServeMux()
promMux.Handle("/metrics", promHandler)
go func() {
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", metricsPort), promMux))
}()
rdb := redis.NewClient(&redis.Options{ rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("localhost:%d", redisPort), Addr: fmt.Sprintf("localhost:%d", redisPort),
Password: "", Password: "",
DB: 0, DB: 0,
}) })
roomsM := rooms.RedisRM { Rdb: rdb, } roomsM := rooms.RedisRM{Rdb: rdb}
sessions := sessions.RedisSM{ Rdb: rdb, } sessions := sessions.RedisSM{Rdb: rdb}
log.Printf("Server will start on port: %d; listening to redis on: %d\n", port, redisPort) log.Printf("Server will start on port: %d; /metrics on %d; listening to redis on: %d\n", port, metricsPort, redisPort)
routes.RegisterRoutes(sessions, roomsM) routes.RegisterRoutes(sessions, roomsM, &metrics)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
} }

43
metrics/metrics.go Normal file
View File

@@ -0,0 +1,43 @@
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
)
type MetricsContainer struct {
Registry *prometheus.Registry
LiveConnectionsGauge prometheus.Gauge
RaiseGestureCounter *prometheus.CounterVec
SpeakerCounter *prometheus.CounterVec
}
const GestureNameLabel string = "gestureString"
func SetupMetrics() MetricsContainer {
registry := prometheus.NewRegistry()
liveConnectionsGauge := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "live_connections_total",
Help: "Total amount of live SSE subscriptions to room state",
})
raiseGestureCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "raise_gesture_total",
Help: "Total amount of raised hands",
}, []string{GestureNameLabel})
speakerCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "speaker_total",
Help: "Total amount of speaker turns",
}, []string{GestureNameLabel})
registry.MustRegister(liveConnectionsGauge, raiseGestureCounter, speakerCounter,
collectors.NewGoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
)
return MetricsContainer{
Registry: registry,
LiveConnectionsGauge: liveConnectionsGauge,
RaiseGestureCounter: raiseGestureCounter,
SpeakerCounter: speakerCounter,
}
}

View File

@@ -53,6 +53,7 @@ func (r *Room) RaiseHand(p PersonId, gesture HandGesture) Room {
// - we should find next speaker // - we should find next speaker
// - assign room.CurrentSpeaker to next speaker or to PersonId(0) to indicate that noone is speaking // - assign room.CurrentSpeaker to next speaker or to PersonId(0) to indicate that noone is speaking
// - if next speaker has gesture of higher priority - set Mark to current speaker for their gesture level // - if next speaker has gesture of higher priority - set Mark to current speaker for their gesture level
// when there is not next speaker, remove all marks
func (r *Room) ReleaseHand(p PersonId) { func (r *Room) ReleaseHand(p PersonId) {
// releasing a hand of a current speaker should result in selection of a new speaker // releasing a hand of a current speaker should result in selection of a new speaker
log.Printf("about to release hand of %d in %+v", p, r) log.Printf("about to release hand of %d in %+v", p, r)
@@ -76,6 +77,7 @@ func (r *Room) ReleaseHand(p PersonId) {
if !nextSpeakerFound { if !nextSpeakerFound {
log.Printf("there is not next speaker, that's ok") log.Printf("there is not next speaker, that's ok")
r.CurrentSpeaker = PersonId(0) r.CurrentSpeaker = PersonId(0)
r.Marks = make(map[HandGesture]PersonId)
} else { } else {
// searching for the next speaker // searching for the next speaker
currentSpeakerGesture := handReleaseGesture currentSpeakerGesture := handReleaseGesture

View File

@@ -20,6 +20,7 @@ var releaseHandTests = []releaseHandTest{
releasingNonSpeakerHand, releasingNonSpeakerHand,
releaseToPersonWithHandAndMark, releaseToPersonWithHandAndMark,
raisingLevelSetMarksWithoutOverridingExisting, raisingLevelSetMarksWithoutOverridingExisting,
releaseAllMarksWhenNoSpeaker,
} }
func TestRoomReleaseHand(t *testing.T) { func TestRoomReleaseHand(t *testing.T) {
@@ -314,3 +315,40 @@ var releaseToPersonWithHandAndMark releaseHandTest = releaseHandTest{
Marks: map[HandGesture]PersonId{}, Marks: map[HandGesture]PersonId{},
}, },
} }
// there is a mark for Expand on person 2, mark for Change Topic on person 3
// speaker is person 1 with Expand
// after hand release person nobody should be speaking, with all marks removed
var releaseAllMarksWhenNoSpeaker releaseHandTest = releaseHandTest{
testName: "releaseAllMarksWhenNoSpeaker",
room: Room{
Name: "test",
CurrentSpeaker: person1.Id,
Paricipants: []PersonId{
person1.Id,
person2.Id,
person3.Id,
person4.Id,
},
ParticipantHands: map[PersonId]HandGesture{
person1.Id: Expand,
},
Marks: map[HandGesture]PersonId{
Expand: person2.Id,
ChangeTopic: person3.Id,
},
},
releasingParticipantId: person1.Id,
expected: Room{
Name: "test",
CurrentSpeaker: PersonId(0),
Paricipants: []PersonId{
person1.Id,
person2.Id,
person3.Id,
person4.Id,
},
ParticipantHands: map[PersonId]HandGesture{},
Marks: map[HandGesture]PersonId{},
},
}

View File

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"log" "log"
"math/rand" "math/rand"
"time"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
@@ -50,13 +51,14 @@ func (r *Room) UnmarshalBinary(data []byte) error {
// let's check whether it will be possible to save nested structs // let's check whether it will be possible to save nested structs
type RoomManager interface { type RoomManager interface {
Get(roomName string) (Room, bool, error) Get(ctx context.Context, roomName string) (Room, bool, error)
Save(room Room) error Save(ctx context.Context, room Room) error
Update(ctx context.Context, roomName string, f func(fromRoom Room) (toRoom Room)) error Update(ctx context.Context, roomName string, f func(fromRoom Room) (toRoom Room)) error
Subscribe(ctx context.Context, roomName string) <-chan Room Subscribe(ctx context.Context, roomName string) <-chan Room
} }
const roomRedisPrefix = "room" const roomRedisPrefix = "room"
const roomTtl = 24 * time.Hour
func roomNameToRedisId(roomName string) string { func roomNameToRedisId(roomName string) string {
return fmt.Sprintf("%s:%s", roomRedisPrefix, roomName) return fmt.Sprintf("%s:%s", roomRedisPrefix, roomName)
@@ -66,9 +68,9 @@ type RedisRM struct {
Rdb *redis.Client Rdb *redis.Client
} }
func (redisRM RedisRM) Get(roomName string) (Room, bool, error) { func (redisRM RedisRM) Get(ctx context.Context, roomName string) (Room, bool, error) {
var readRoom Room var readRoom Room
err := redisRM.Rdb.Get(context.TODO(), roomNameToRedisId(roomName)).Scan(&readRoom) err := redisRM.Rdb.Get(ctx, roomNameToRedisId(roomName)).Scan(&readRoom)
if err == redis.Nil { if err == redis.Nil {
return Room{}, false, nil return Room{}, false, nil
} }
@@ -155,7 +157,7 @@ func (redisRM RedisRM) Update(ctx context.Context, roomName string, f func(fromR
_, err = tx.Pipelined(ctx, func(pipe redis.Pipeliner) error { _, err = tx.Pipelined(ctx, func(pipe redis.Pipeliner) error {
log.Printf(">> about to Set %s to %v", roomName, room) log.Printf(">> about to Set %s to %v", roomName, room)
pipe.Set(ctx, roomKey, &room, 0) pipe.Set(ctx, roomKey, &room, roomTtl)
return nil return nil
}) })
@@ -177,7 +179,7 @@ func (redisRM RedisRM) Update(ctx context.Context, roomName string, f func(fromR
return errors.New("update reached maximum amount of retries") return errors.New("update reached maximum amount of retries")
} }
func (redisRM RedisRM) Save(room Room) error { func (redisRM RedisRM) Save(ctx context.Context, room Room) error {
err := redisRM.Rdb.Set(context.TODO(), roomNameToRedisId(room.Name), &room, 0).Err() // maybe even set expiration? err := redisRM.Rdb.Set(ctx, roomNameToRedisId(room.Name), &room, roomTtl).Err()
return err return err
} }

View File

@@ -34,6 +34,7 @@ func indexPageRoute(
) http.HandlerFunc { ) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, err := getRequestSession(r, sessionSM) session, err := getRequestSession(r, sessionSM)
log.Printf("/ the session i got is %+v", session)
if err != nil { if err != nil {
log.Printf("/ session not found, means should render the login section %s", err) log.Printf("/ session not found, means should render the login section %s", err)
// TODO return error i guess // TODO return error i guess
@@ -53,7 +54,7 @@ func indexPageRoute(
data := pageData{ data := pageData{
Base: baseData{ Base: baseData{
Title: "hello base template title", Title: "Some Automoderation: simple automation",
}, },
Header: headerData{ Header: headerData{
Title: session.RoomId, Title: session.RoomId,

View File

@@ -71,7 +71,7 @@ func getRequestSession(r *http.Request,
if err != nil { if err != nil {
return sessions.SessionData{}, ErrAuthCookieValueInvalid return sessions.SessionData{}, ErrAuthCookieValueInvalid
} }
session := sessionsM.Get(sessionId) session := sessionsM.Get(r.Context(), sessionId)
if session == (sessions.SessionData{}) { if session == (sessions.SessionData{}) {
return sessions.SessionData{}, ErrAuthSessionNotFound return sessions.SessionData{}, ErrAuthSessionNotFound
} }
@@ -115,7 +115,7 @@ func renderLoginPage(w http.ResponseWriter, roomName string, isRoomExisting bool
title := "Some Automoderation: Join room or create one" title := "Some Automoderation: Join room or create one"
if roomName != "" { if roomName != "" {
title = fmt.Sprintf("Some Automoderation: create or join '%s' room", roomName) title = fmt.Sprintf("Some Automoderation: join room '%s'", roomName)
} }
data := pageData{ data := pageData{
Base: baseData{ Base: baseData{
@@ -146,7 +146,7 @@ func createRoomHandler(templateFs *embed.FS,
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
} }
roomName := r.PostFormValue("roomName") roomName := r.PostFormValue("roomName")
_, exists, _ := roomsM.Get(roomName) _, exists, _ := roomsM.Get(r.Context(), roomName)
if exists { if exists {
// TODO return anouther error notice // TODO return anouther error notice
log.Printf("error, room name occupied %s", roomName) log.Printf("error, room name occupied %s", roomName)
@@ -171,12 +171,13 @@ func createRoomHandler(templateFs *embed.FS,
AllKnownPeople: map[rooms.PersonId]rooms.Person{ AllKnownPeople: map[rooms.PersonId]rooms.Person{
person.Id: person}, person.Id: person},
} }
err = roomsM.Save(newRoom) err = roomsM.Save(r.Context(), newRoom)
if err != nil { if err != nil {
log.Printf("what am i to do? error saving room %s", err) log.Printf("what am i to do? error saving room %s", err)
// todo return error notice somehow // todo return error notice somehow
} }
newSession, err := sessionSM.Save(r.Context(), newRoom.Name, person.Id) newSession, err := sessionSM.Save(r.Context(), newRoom.Name, person.Id)
log.Printf("saving session %v", newSession)
if err != nil { if err != nil {
log.Printf("what am i to do? error saving session %s", err) log.Printf("what am i to do? error saving session %s", err)
// todo return error notice somehow // todo return error notice somehow
@@ -187,6 +188,7 @@ func createRoomHandler(templateFs *embed.FS,
Secure: true, Secure: true,
HttpOnly: true, HttpOnly: true,
Path: "/", Path: "/",
Expires: time.Now().Add(newSession.ExpireIn),
}) })
w.Header().Add("HX-Redirect", fmt.Sprintf("/room/%s", newRoom.Name)) w.Header().Add("HX-Redirect", fmt.Sprintf("/room/%s", newRoom.Name))
} }
@@ -203,7 +205,7 @@ func checkRoomName(templateFs *embed.FS,
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
} }
roomName := r.PostFormValue("roomName") roomName := r.PostFormValue("roomName")
_, isFound, err := roomsM.Get(roomName) _, isFound, err := roomsM.Get(r.Context(), roomName)
if err != nil { if err != nil {
log.Printf("/login/room-name-check error finding room %s\n", err) log.Printf("/login/room-name-check error finding room %s\n", err)
} }
@@ -229,7 +231,7 @@ func joinRoomHandler(templateFs *embed.FS,
personPass := r.PostFormValue("personalPassword") personPass := r.PostFormValue("personalPassword")
// a) get room data // a) get room data
room, _, err := roomsM.Get(roomName) room, _, err := roomsM.Get(r.Context(), roomName)
if err != nil { if err != nil {
log.Printf("/login/join error getting room %s", roomName) log.Printf("/login/join error getting room %s", roomName)
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
@@ -308,6 +310,7 @@ func joinRoomHandler(templateFs *embed.FS,
} }
newSession, err := sessionSM.Save(r.Context(), room.Name, person.Id) newSession, err := sessionSM.Save(r.Context(), room.Name, person.Id)
log.Printf("saving session %v", newSession)
if err != nil { if err != nil {
log.Printf("/login/submit > error saving session %s", err) log.Printf("/login/submit > error saving session %s", err)
} }
@@ -317,6 +320,7 @@ func joinRoomHandler(templateFs *embed.FS,
Secure: true, Secure: true,
HttpOnly: true, HttpOnly: true,
Path: "/", Path: "/",
Expires: time.Now().Add(newSession.ExpireIn),
}) })
log.Printf("is is %+v. room things %s & %s, personal things %s and %s. \n found room %+v", log.Printf("is is %+v. room things %s & %s, personal things %s and %s. \n found room %+v",
newSession, roomName, roomPass, personName, personPass, room, newSession, roomName, roomPass, personName, personPass, room,

View File

@@ -10,6 +10,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sunshine.industries/some-automoderation/metrics"
"sunshine.industries/some-automoderation/rooms" "sunshine.industries/some-automoderation/rooms"
"sunshine.industries/some-automoderation/sessions" "sunshine.industries/some-automoderation/sessions"
) )
@@ -23,6 +24,7 @@ func registerPageRoutes(
templateFs *embed.FS, templateFs *embed.FS,
sessionSM sessions.SessionManagement, sessionSM sessions.SessionManagement,
roomsM rooms.RoomManager, roomsM rooms.RoomManager,
metrics *metrics.MetricsContainer,
) { ) {
http.Handle(roomPath, // ending in / captures all following path sections, i.e room name http.Handle(roomPath, // ending in / captures all following path sections, i.e room name
http.StripPrefix(roomPath, roomPageRoute(templateFs, roomsM, sessionSM))) http.StripPrefix(roomPath, roomPageRoute(templateFs, roomsM, sessionSM)))
@@ -30,15 +32,15 @@ func registerPageRoutes(
http.Handle(raiseHandPath, // ending in / captures all following path sections, i.e gesture num http.Handle(raiseHandPath, // ending in / captures all following path sections, i.e gesture num
authedPageMiddleware( authedPageMiddleware(
sessionSM, sessionSM,
http.StripPrefix(raiseHandPath, raiseGestureHandRoute(roomsM)))) http.StripPrefix(raiseHandPath, raiseGestureHandRoute(roomsM, metrics))))
http.Handle("/rooms/releaseHand", http.Handle("/rooms/releaseHand",
authedPageMiddleware(sessionSM, releaseHandRoute(roomsM))) authedPageMiddleware(sessionSM, releaseHandRoute(roomsM, metrics)))
http.Handle(subscribeRoomPath, http.Handle(subscribeRoomPath,
authedPageMiddleware( authedPageMiddleware(
sessionSM, sessionSM,
http.StripPrefix(subscribeRoomPath, streamingRoomStates(templateFs, roomsM)))) http.StripPrefix(subscribeRoomPath, streamingRoomStates(templateFs, roomsM, metrics))))
http.HandleFunc("/rooms/preview-templates", roomTemplatesPreview(templateFs)) http.HandleFunc("/rooms/preview-templates", roomTemplatesPreview(templateFs))
@@ -48,8 +50,11 @@ func registerPageRoutes(
func streamingRoomStates( func streamingRoomStates(
templateFs *embed.FS, templateFs *embed.FS,
roomsM rooms.RoomManager, roomsM rooms.RoomManager,
metrics *metrics.MetricsContainer,
) http.HandlerFunc { ) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
metrics.LiveConnectionsGauge.Inc()
defer metrics.LiveConnectionsGauge.Dec()
r.ParseForm() r.ParseForm()
roomName := r.FormValue("roomName") roomName := r.FormValue("roomName")
defer log.Printf("/rooms/subscribe/%s stream ended\n", roomName) defer log.Printf("/rooms/subscribe/%s stream ended\n", roomName)
@@ -112,6 +117,7 @@ func streamingRoomStates(
// TODO should return control for raised state // TODO should return control for raised state
func raiseGestureHandRoute( func raiseGestureHandRoute(
roomsM rooms.RoomManager, roomsM rooms.RoomManager,
metrics *metrics.MetricsContainer,
) http.HandlerFunc { ) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
gestureInd, err := strconv.Atoi(r.URL.Path) gestureInd, err := strconv.Atoi(r.URL.Path)
@@ -120,6 +126,7 @@ func raiseGestureHandRoute(
log.Printf("/rooms/raiseGesture error %s gettin hand symbol index from path %s\n", err, r.URL.Path) log.Printf("/rooms/raiseGesture error %s gettin hand symbol index from path %s\n", err, r.URL.Path)
return return
} }
metrics.RaiseGestureCounter.WithLabelValues(selectedGesture.String()).Inc()
log.Printf("/rooms/raiseGesture successfully got gesture %d : %s", selectedGesture, selectedGesture.String()) log.Printf("/rooms/raiseGesture successfully got gesture %d : %s", selectedGesture, selectedGesture.String())
session, found := getContextSession(r.Context()) session, found := getContextSession(r.Context())
if !found { if !found {
@@ -129,6 +136,10 @@ func raiseGestureHandRoute(
} }
var room rooms.Room var room rooms.Room
err = roomsM.Update(r.Context(), session.RoomId, func(fromRoom rooms.Room) (toRoom rooms.Room) { err = roomsM.Update(r.Context(), session.RoomId, func(fromRoom rooms.Room) (toRoom rooms.Room) {
if fromRoom.CurrentSpeaker == rooms.PersonId(0) {
// room had no speaker, need count new speaker turn
metrics.SpeakerCounter.WithLabelValues(selectedGesture.String()).Inc()
}
toRoom = fromRoom.RaiseHand(session.PersonId, selectedGesture) toRoom = fromRoom.RaiseHand(session.PersonId, selectedGesture)
room = toRoom room = toRoom
return toRoom return toRoom
@@ -167,6 +178,7 @@ func raiseGestureHandRoute(
// TODO should return lowered control for passed hand gesture, i guess optional // TODO should return lowered control for passed hand gesture, i guess optional
func releaseHandRoute( func releaseHandRoute(
roomsM rooms.RoomManager, roomsM rooms.RoomManager,
metrics *metrics.MetricsContainer,
) http.HandlerFunc { ) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, found := getContextSession(r.Context()) session, found := getContextSession(r.Context())
@@ -179,6 +191,10 @@ func releaseHandRoute(
err := roomsM.Update(r.Context(), session.RoomId, func(fromRoom rooms.Room) (toRoom rooms.Room) { err := roomsM.Update(r.Context(), session.RoomId, func(fromRoom rooms.Room) (toRoom rooms.Room) {
toRoom = fromRoom toRoom = fromRoom
toRoom.ReleaseHand(session.PersonId) toRoom.ReleaseHand(session.PersonId)
if fromRoom.CurrentSpeaker != toRoom.CurrentSpeaker && toRoom.CurrentSpeaker != rooms.PersonId(0) {
gesture := toRoom.ParticipantHands[toRoom.CurrentSpeaker]
metrics.SpeakerCounter.WithLabelValues(gesture.String()).Inc()
}
return toRoom return toRoom
}) })
if err != nil { if err != nil {
@@ -223,7 +239,7 @@ func roomPageRoute(
// check session, // check session,
session, err := getRequestSession(r, sessionSM) session, err := getRequestSession(r, sessionSM)
room, found, err := roomsM.Get(roomName) room, found, err := roomsM.Get(r.Context(), roomName)
if err != nil || session.RoomId != roomName { if err != nil || session.RoomId != roomName {
log.Printf("not authed with session %+v | error %s, but for wrong room, trying to access %s", session, err, roomName) log.Printf("not authed with session %+v | error %s, but for wrong room, trying to access %s", session, err, roomName)
renderLoginPage(w, roomName, found) renderLoginPage(w, roomName, found)
@@ -268,7 +284,7 @@ func roomPageRoute(
} }
data := pageData{ data := pageData{
Base: baseData{ Base: baseData{
Title: "room-lala-from-base", Title: fmt.Sprintf("Some Automoderation: discussion in '%s'", room.Name),
LoggedIn: true, LoggedIn: true,
}, },
Content: contentData, Content: contentData,

View File

@@ -4,6 +4,7 @@ import (
"embed" "embed"
"net/http" "net/http"
"sunshine.industries/some-automoderation/metrics"
"sunshine.industries/some-automoderation/rooms" "sunshine.industries/some-automoderation/rooms"
"sunshine.industries/some-automoderation/sessions" "sunshine.industries/some-automoderation/sessions"
) )
@@ -14,7 +15,7 @@ var templateFs embed.FS
//go:embed static //go:embed static
var staticFilesFs embed.FS var staticFilesFs embed.FS
func RegisterRoutes(sessionsM sessions.SessionManagement, rooms rooms.RoomManager) { func RegisterRoutes(sessionsM sessions.SessionManagement, rooms rooms.RoomManager, metrics *metrics.MetricsContainer) {
// login page // login page
registerLoginRoutes(&templateFs, sessionsM, rooms) registerLoginRoutes(&templateFs, sessionsM, rooms)
@@ -22,7 +23,7 @@ func RegisterRoutes(sessionsM sessions.SessionManagement, rooms rooms.RoomManage
http.Handle("/", indexPageRoute(&templateFs, sessionsM, rooms)) http.Handle("/", indexPageRoute(&templateFs, sessionsM, rooms))
// main conversation room page // main conversation room page
registerPageRoutes(&templateFs, sessionsM, rooms) registerPageRoutes(&templateFs, sessionsM, rooms, metrics)
// static resources route // static resources route
http.Handle("/static/", http.Handle("/static/",

View File

@@ -16,15 +16,18 @@ type SessionData struct {
SessionId int `redis:"session_id"` SessionId int `redis:"session_id"`
RoomId string `redis:"room_id"` RoomId string `redis:"room_id"`
PersonId rooms.PersonId `redis:"person_id"` PersonId rooms.PersonId `redis:"person_id"`
ExpireIn time.Duration `redis:"-"`
} }
type SessionManagement interface { type SessionManagement interface {
Get(sessionId int) SessionData Get(ctx context.Context, sessionId int) SessionData
Save(ctx context.Context, roomName string, personId rooms.PersonId) (SessionData, error) Save(ctx context.Context, roomName string, personId rooms.PersonId) (SessionData, error)
Remove(ctx context.Context, sessionId int) error Remove(ctx context.Context, sessionId int) error
} }
const sessionPrefix = "session" const sessionPrefix = "session"
const SessionTtl = 3 * time.Hour
const SessionProlongationWindow = time.Hour
func sessionIdToKey(sessionId int) string { func sessionIdToKey(sessionId int) string {
return fmt.Sprintf("%s:%d", sessionPrefix, sessionId) return fmt.Sprintf("%s:%d", sessionPrefix, sessionId)
@@ -34,15 +37,34 @@ type RedisSM struct {
Rdb *redis.Client Rdb *redis.Client
} }
func (redisSM RedisSM) Get(sessionId int) SessionData { func (redisSM RedisSM) Get(ctx context.Context, sessionId int) SessionData {
var foundSession SessionData var foundSession SessionData
redisKey := sessionIdToKey(sessionId) redisKey := sessionIdToKey(sessionId)
err := redisSM.Rdb.HGetAll(context.TODO(), redisKey).Scan(&foundSession) err := redisSM.Rdb.HGetAll(ctx, redisKey).Scan(&foundSession)
if err != nil { if err != nil {
log.Printf("> error reading %s : %s", redisKey, err) log.Printf("> error reading %s : %s", redisKey, err)
return SessionData{} return SessionData{}
} }
log.Printf("> successfully found %d %+v", sessionId, foundSession) log.Printf("> successfully found %d %+v", sessionId, foundSession)
ttl, err := redisSM.Rdb.TTL(ctx, redisKey).Result()
if err != nil {
log.Printf("> error getting ttl for session %+v", foundSession)
return foundSession
}
if ttl == -2 {
log.Printf("> ttl indicates session doesn't exist for session %+v", foundSession)
return SessionData{}
}
if ttl < SessionProlongationWindow {
err = redisSM.Rdb.Expire(ctx, redisKey, SessionTtl).Err()
if err != nil {
log.Printf("> error updating ttl for session %+v", foundSession)
return foundSession
} else {
ttl = SessionTtl
}
}
foundSession.ExpireIn = ttl
return foundSession return foundSession
} }
func (redisSM RedisSM) Save(ctx context.Context, roomName string, personId rooms.PersonId) (SessionData, error) { func (redisSM RedisSM) Save(ctx context.Context, roomName string, personId rooms.PersonId) (SessionData, error) {
@@ -51,9 +73,10 @@ func (redisSM RedisSM) Save(ctx context.Context, roomName string, personId rooms
SessionId: randId, SessionId: randId,
RoomId: roomName, RoomId: roomName,
PersonId: personId, PersonId: personId,
ExpireIn: SessionTtl,
} }
err := redisSM.Rdb.HSet(ctx, sessionIdToKey(randId), newSession).Err() err := redisSM.Rdb.HSet(ctx, sessionIdToKey(randId), newSession).Err()
redisSM.Rdb.Expire(ctx, sessionIdToKey(randId), 24 * time.Hour) redisSM.Rdb.Expire(ctx, sessionIdToKey(randId), SessionTtl)
if err != nil { if err != nil {
log.Printf("> error! saving session %+v %s", newSession, err) log.Printf("> error! saving session %+v %s", newSession, err)
@@ -69,7 +92,7 @@ func (redisSM RedisSM) Remove(ctx context.Context, sessionId int) error {
type DummySM struct{} type DummySM struct{}
func (d DummySM) Get(sessionId int) SessionData { func (d DummySM) Get(ctx context.Context, sessionId int) SessionData {
log.Printf("get dummy session by %d", sessionId) log.Printf("get dummy session by %d", sessionId)
return SessionData{} return SessionData{}
} }