now using maps of Guid to Agent/Client in the state, working towards the definitive solution.

Using LinkedMap that preserves insertion order for the implementation and also added unit tests for that.
This commit is contained in:
Erik Brakkee 2024-08-15 21:04:31 +02:00
parent 98f6b414de
commit fd18a63360
13 changed files with 358 additions and 54 deletions

View File

@ -13,6 +13,9 @@ generate:
vet: fmt vet: fmt
go vet ./... go vet ./...
test:
go test -v ./...
build: generate vet build: generate vet
mkdir -p bin mkdir -p bin
go build -o bin ./cmd/... go build -o bin ./cmd/...

View File

@ -15,6 +15,6 @@ func NewStateNotifier() *StateNotifier {
} }
func (notifier StateNotifier) Publish(state *models.State) { func (notifier StateNotifier) Publish(state *models.State) {
notifier.webNotificationChannel <- state.Copy() notifier.webNotificationChannel <- state
notifier.prometheusNotificationChannel <- state.Copy() notifier.prometheusNotificationChannel <- state
} }

View File

@ -2,6 +2,7 @@ package main
import ( import (
"git.wamblee.org/converge/pkg/models" "git.wamblee.org/converge/pkg/models"
"git.wamblee.org/converge/pkg/support/collections"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
@ -14,20 +15,20 @@ const NAMESPACE = "converge"
// more efficient state representation for state // more efficient state representation for state
type PrometheusState struct { type PrometheusState struct {
agents map[models.AgentGuid]*models.Agent agents *collections.LinkedMap[models.AgentGuid, *models.Agent]
clients map[models.ClientGuid]*models.Client clients *collections.LinkedMap[models.ClientGuid, *models.Client]
} }
func NewPrometheusState(state *models.State) *PrometheusState { func NewPrometheusState(state *models.State) *PrometheusState {
res := PrometheusState{ res := PrometheusState{
agents: make(map[models.AgentGuid]*models.Agent), agents: collections.NewLinkedMap[models.AgentGuid, *models.Agent](),
clients: make(map[models.ClientGuid]*models.Client), clients: collections.NewLinkedMap[models.ClientGuid, *models.Client](),
} }
for i, _ := range state.Agents { for agent := range state.Agents.RangeValues() {
res.agents[state.Agents[i].Guid] = &state.Agents[i] res.agents.Put(agent.Guid, agent)
} }
for i, _ := range state.Clients { for client := range state.Clients.RangeValues() {
res.clients[state.Clients[i].Guid] = &state.Clients[i] res.clients.Put(client.Guid, client)
} }
return &res return &res
} }
@ -35,7 +36,7 @@ func NewPrometheusState(state *models.State) *PrometheusState {
var ( var (
// remember previous values of agent guids and clients so that we can increment // remember previous values of agent guids and clients so that we can increment
// the cumulative counters. // the cumulative counters.
lastState *PrometheusState = NewPrometheusState(&models.State{}) lastState *PrometheusState = NewPrometheusState(models.NewState())
cumulativeAgentCount = promauto.NewCounter(prometheus.CounterOpts{ cumulativeAgentCount = promauto.NewCounter(prometheus.CounterOpts{
Namespace: NAMESPACE, Namespace: NAMESPACE,
@ -149,7 +150,7 @@ func clientLabels(client *models.Client) prometheus.Labels {
} }
func agentActive(agent *models.Agent) { func agentActive(agent *models.Agent) {
prevAgent, ok := lastState.agents[agent.Guid] prevAgent, ok := lastState.agents.Get(agent.Guid)
if ok && *prevAgent != *agent { if ok && *prevAgent != *agent {
removeAgentInfoMetrics(prevAgent) removeAgentInfoMetrics(prevAgent)
} }
@ -164,7 +165,7 @@ func agentActive(agent *models.Agent) {
} }
func clientActive(client *models.Client) { func clientActive(client *models.Client) {
prevClient, ok := lastState.clients[client.Guid] prevClient, ok := lastState.clients.Get(client.Guid)
if ok && *prevClient != *client { if ok && *prevClient != *client {
removeClientInfoMetrics(prevClient) removeClientInfoMetrics(prevClient)
} }
@ -222,12 +223,12 @@ func updateMetrics(state *models.State) {
} }
func updateDurations() { func updateDurations() {
for _, agent := range lastState.agents { for agent := range lastState.agents.RangeValues() {
agentDuration. agentDuration.
With(prometheus.Labels{"agent_guid": string(agent.Guid)}). With(prometheus.Labels{"agent_guid": string(agent.Guid)}).
Set(float64(time.Now().Sub(agent.StartTime).Seconds())) Set(float64(time.Now().Sub(agent.StartTime).Seconds()))
} }
for _, client := range lastState.clients { for client := range lastState.clients.RangeValues() {
clientDuration. clientDuration.
With(prometheus.Labels{"client_guid": string(client.Guid)}). With(prometheus.Labels{"client_guid": string(client.Guid)}).
Set(float64(time.Now().Sub(client.StartTime).Seconds())) Set(float64(time.Now().Sub(client.StartTime).Seconds()))
@ -239,13 +240,13 @@ func updateMetricsImpl(state *PrometheusState) {
agentGuids := make(map[models.AgentGuid]*models.Agent) agentGuids := make(map[models.AgentGuid]*models.Agent)
clientGuids := make(map[models.ClientGuid]*models.Client) clientGuids := make(map[models.ClientGuid]*models.Client)
agentCount.Set(float64(len(state.agents))) agentCount.Set(float64(state.agents.Len()))
disconnectedAgents := make(map[models.AgentGuid]*models.Agent) disconnectedAgents := make(map[models.AgentGuid]*models.Agent)
for _, agent := range lastState.agents { for agent := range lastState.agents.RangeValues() {
disconnectedAgents[agent.Guid] = agent disconnectedAgents[agent.Guid] = agent
} }
for _, agent := range state.agents { for agent := range state.agents.RangeValues() {
if lastState.agents[agent.Guid] == nil { if lastState.agents.Contains(agent.Guid) {
cumulativeAgentCount.Inc() cumulativeAgentCount.Inc()
} }
delete(disconnectedAgents, agent.Guid) delete(disconnectedAgents, agent.Guid)
@ -256,15 +257,15 @@ func updateMetricsImpl(state *PrometheusState) {
removeAgentMetrics(agent) removeAgentMetrics(agent)
} }
clientCount.Set(float64(len(state.clients))) clientCount.Set(float64(state.clients.Len()))
// with this app // with this app
disconnectedClients := make(map[models.ClientGuid]*models.Client) disconnectedClients := make(map[models.ClientGuid]*models.Client)
for _, client := range lastState.clients { for client := range lastState.clients.RangeValues() {
disconnectedClients[client.Guid] = client disconnectedClients[client.Guid] = client
} }
for _, client := range state.clients { for client := range state.clients.RangeValues() {
if lastState.clients[client.Guid] == nil { if lastState.clients.Contains(client.Guid) {
cumulativeClientCount.Inc() cumulativeClientCount.Inc()
} }
delete(disconnectedClients, client.Guid) delete(disconnectedClients, client.Guid)

View File

@ -79,7 +79,7 @@ func main() {
panic(err) panic(err)
} }
state := models.State{} state := models.NewState()
agent := models.Agent{ agent := models.Agent{
Guid: models.AgentGuid(strconv.Itoa(rand.Int())), Guid: models.AgentGuid(strconv.Itoa(rand.Int())),
RemoteAddr: "10.220.1.3:3333", RemoteAddr: "10.220.1.3:3333",
@ -94,7 +94,7 @@ func main() {
}, },
ExpiryTime: time.Now().In(japan).Add(10 * time.Minute), ExpiryTime: time.Now().In(japan).Add(10 * time.Minute),
} }
state.Agents = append(state.Agents, agent) state.Agents.Put(agent.Guid, &agent)
client := models.Client{ client := models.Client{
Guid: models.ClientGuid(strconv.Itoa(rand.Int())), Guid: models.ClientGuid(strconv.Itoa(rand.Int())),
RemoteAddr: models.RemoteAddr("10.1.3.3"), RemoteAddr: models.RemoteAddr("10.1.3.3"),
@ -104,7 +104,7 @@ func main() {
StartTime: time.Now().In(japan), StartTime: time.Now().In(japan),
SessionType: models.SessionType("sftp"), SessionType: models.SessionType("sftp"),
} }
state.Clients = append(state.Clients, client) state.Clients.Put(client.Guid, &client)
return templates2.SessionsTab(&state, netherlands) return templates2.SessionsTab(state, netherlands)
}) })
} }

5
go.mod
View File

@ -12,6 +12,7 @@ require (
github.com/hashicorp/yamux v0.1.1 github.com/hashicorp/yamux v0.1.1
github.com/pkg/sftp v1.13.6 github.com/pkg/sftp v1.13.6
github.com/prometheus/client_golang v1.19.1 github.com/prometheus/client_golang v1.19.1
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.25.0 golang.org/x/crypto v0.25.0
golang.org/x/term v0.22.0 golang.org/x/term v0.22.0
) )
@ -21,10 +22,14 @@ require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/beorn7/perks v1.0.1 // indirect 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/davecgh/go-spew v1.1.1 // indirect
github.com/kr/fs v0.1.0 // indirect github.com/kr/fs v0.1.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect
golang.org/x/sys v0.22.0 // indirect golang.org/x/sys v0.22.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

13
go.sum
View File

@ -10,6 +10,7 @@ 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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
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.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -27,6 +28,10 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -39,12 +44,14 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
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=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@ -83,6 +90,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -2,6 +2,7 @@ package models
import ( import (
"git.wamblee.org/converge/pkg/comms" "git.wamblee.org/converge/pkg/comms"
"git.wamblee.org/converge/pkg/support/collections"
"time" "time"
) )
@ -39,15 +40,13 @@ type Client struct {
// Created by the server and used for updating the web client // Created by the server and used for updating the web client
// and prometheus metrics. // and prometheus metrics.
type State struct { type State struct {
Agents []Agent Agents *collections.LinkedMap[AgentGuid, *Agent]
Clients []Client Clients *collections.LinkedMap[ClientGuid, *Client]
} }
func (state *State) Copy() *State { func NewState() *State {
c := State{} return &State{
c.Agents = make([]Agent, len(state.Agents)) Agents: collections.NewLinkedMap[AgentGuid, *Agent](),
c.Clients = make([]Client, len(state.Clients)) Clients: collections.NewLinkedMap[ClientGuid, *Client](),
copy(c.Agents, state.Agents) }
copy(c.Clients, state.Clients)
return &c
} }

View File

@ -81,16 +81,14 @@ func NewAdmin() *Admin {
func (admin *Admin) CreateNotifification() *models.State { func (admin *Admin) CreateNotifification() *models.State {
admin.mutex.Lock() admin.mutex.Lock()
defer admin.mutex.Unlock() defer admin.mutex.Unlock()
state := models.State{} state := models.NewState()
state.Agents = make([]models.Agent, 0, len(admin.agents))
state.Clients = make([]models.Client, 0, len(admin.clients))
for _, agent := range admin.agents { for _, agent := range admin.agents {
state.Agents = append(state.Agents, agent.Agent) state.Agents.Put(agent.Guid, &agent.Agent)
} }
for _, client := range admin.clients { for _, client := range admin.clients {
state.Clients = append(state.Clients, client.Client) state.Clients.Put(client.Guid, &client.Client)
} }
return &state return state
} }
func (admin *Admin) getFreeId(publicId models.RendezVousId) (models.RendezVousId, error) { func (admin *Admin) getFreeId(publicId models.RendezVousId) (models.RendezVousId, error) {

View File

@ -134,7 +134,7 @@ func logStatusImpl(admin *models.State, notifier Notifier) {
lines = append(lines, fmt.Sprintf(format, "AGENT", "ACTIVE_SINCE", "EXPIRY_TIME", lines = append(lines, fmt.Sprintf(format, "AGENT", "ACTIVE_SINCE", "EXPIRY_TIME",
"USER", "HOST", "OS")) "USER", "HOST", "OS"))
for _, agent := range admin.Agents { for agent := range admin.Agents.RangeValues() {
lines = append(lines, fmt.Sprintf(format, agent.PublicId, lines = append(lines, fmt.Sprintf(format, agent.PublicId,
agent.StartTime.Format(time.DateTime), agent.StartTime.Format(time.DateTime),
agent.ExpiryTime.Format(time.DateTime), agent.ExpiryTime.Format(time.DateTime),
@ -145,7 +145,7 @@ func logStatusImpl(admin *models.State, notifier Notifier) {
lines = append(lines, "") lines = append(lines, "")
format = "%-10s %-20s %-20s %-20s %-20s" format = "%-10s %-20s %-20s %-20s %-20s"
lines = append(lines, fmt.Sprintf(format, "CLIENT", "AGENT", "ACTIVE_SINCE", "REMOTE_ADDRESS", "SESSION_TYPE")) lines = append(lines, fmt.Sprintf(format, "CLIENT", "AGENT", "ACTIVE_SINCE", "REMOTE_ADDRESS", "SESSION_TYPE"))
for _, client := range admin.Clients { for client := range admin.Clients.RangeValues() {
lines = append(lines, fmt.Sprintf(format, lines = append(lines, fmt.Sprintf(format,
client.ClientId, client.ClientId,
client.PublicId, client.PublicId,

View File

@ -123,7 +123,7 @@ func (session *WebSession) WriteNotifications(location *time.Location, ctx conte
log.Println("channel closed") log.Println("channel closed")
return return
} }
if session.writeNotificationToClient(location, notification) { if !session.writeNotificationToClient(location, notification) {
return return
} }
case <-timer.C: case <-timer.C:
@ -140,9 +140,9 @@ func (session *WebSession) writeNotificationToClient(location *time.Location, no
err := templates.State(notification, location).Render(context.Background(), session.conn) err := templates.State(notification, location).Render(context.Background(), session.conn)
if err != nil { if err != nil {
log.Printf("WS connection closed: %v", err) log.Printf("WS connection closed: %v", err)
return true
}
return false return false
}
return true
} }
func (sessions *WebSessions) SessionClosed(session *WebSession) { func (sessions *WebSessions) SessionClosed(session *WebSession) {

View File

@ -31,7 +31,7 @@ templ State(state *models.State, location *time.Location) {
<h3>agents</h3> <h3>agents</h3>
if len(state.Agents) == 0 { if state.Agents.Len() == 0 {
<p>-</p> <p>-</p>
} else { } else {
<table class="table"> <table class="table">
@ -46,7 +46,7 @@ templ State(state *models.State, location *time.Location) {
<th>shell</th> <th>shell</th>
</tr> </tr>
</thead> </thead>
for _, agent := range state.Agents { for agent := range state.Agents.RangeValues() {
<tr> <tr>
<td>{string(agent.PublicId)}</td> <td>{string(agent.PublicId)}</td>
<td>{agent.StartTime.In(location).Format(time.DateTime)}</td> <td>{agent.StartTime.In(location).Format(time.DateTime)}</td>
@ -64,7 +64,7 @@ templ State(state *models.State, location *time.Location) {
<h3>clients</h3> <h3>clients</h3>
if len(state.Clients) == 0 { if state.Clients.Len() == 0 {
<p>-</p> <p>-</p>
} else { } else {
<table class="table"> <table class="table">
@ -80,7 +80,7 @@ templ State(state *models.State, location *time.Location) {
<th>shell</th> <th>shell</th>
</tr> </tr>
</thead> </thead>
for _, client := range state.Clients { for client := range state.Clients.RangeValues() {
<tr> <tr>
<td>{string(client.ClientId)}</td> <td>{string(client.ClientId)}</td>
<td>{client.StartTime.In(location).Format(time.DateTime)}</td> <td>{client.StartTime.In(location).Format(time.DateTime)}</td>

View File

@ -0,0 +1,159 @@
package collections
import (
"fmt"
)
// similar to linkes hash map in Java, a map that preserves insertion order
var checkStatus = false
type Node[K comparable, V any] struct {
key K
value V
prev *Node[K, V]
next *Node[K, V]
}
type LinkedMap[K comparable, V any] struct {
first *Node[K, V]
last *Node[K, V]
collection map[K]*Node[K, V]
}
func NewLinkedMap[K comparable, V any]() *LinkedMap[K, V] {
res := LinkedMap[K, V]{
first: nil,
last: nil,
collection: make(map[K]*Node[K, V]),
}
res.check()
return &res
}
func (m *LinkedMap[K, V]) Len() int {
return len(m.collection)
}
func (m *LinkedMap[K, V]) Put(key K, value V) {
defer m.check()
newNode := &Node[K, V]{
key: key,
value: value,
prev: m.last,
next: nil,
}
if m.first == nil {
m.first = newNode
m.last = m.first
m.collection[key] = m.first
return
}
m.Delete(key)
m.last.next = newNode
m.last = newNode
m.collection[key] = newNode
}
func (m *LinkedMap[K, V]) Delete(key K) bool {
defer m.check()
node, ok := m.collection[key]
if !ok {
return false
}
if node.prev != nil {
node.prev.next = node.next
} else {
m.first = node.next
}
if node.next != nil {
node.next.prev = node.prev
} else {
m.last = node.prev
}
delete(m.collection, key)
return true
}
func (m *LinkedMap[K, V]) Get(key K) (V, bool) {
defer m.check()
v, ok := m.collection[key]
if !ok {
return *new(V), false
}
return v.value, true
}
func (m *LinkedMap[K, V]) Contains(key K) bool {
_, ok := m.collection[key]
return ok
}
type Entry[K comparable, V any] struct {
Key K
Value V
}
func (m *LinkedMap[K, V]) RangeKeys() <-chan K {
defer m.check()
res := make(chan K, len(m.collection))
for node := m.first; node != nil; node = node.next {
res <- node.key
}
close(res)
return res
}
func (m *LinkedMap[K, V]) RangeValues() <-chan V {
defer m.check()
res := make(chan V, len(m.collection))
for node := m.first; node != nil; node = node.next {
res <- node.value
}
close(res)
return res
}
func (m *LinkedMap[K, V]) RangeEntries() <-chan Entry[K, V] {
defer m.check()
res := make(chan Entry[K, V], len(m.collection))
for node := m.first; node != nil; node = node.next {
res <- Entry[K, V]{
Key: node.key,
Value: node.value,
}
}
close(res)
return res
}
func (m *LinkedMap[K, V]) check() {
if !checkStatus {
return
}
assert := func(c bool, text string) {
if !c {
panic(text)
}
}
if m.first == nil {
assert(m.last == nil, "Last should be nil")
}
if m.first != nil {
assert(m.last != nil, "Last must not be nil")
}
if m.first == nil {
assert(m.Len() == 0, "Len should be 0")
}
if m.first != nil {
assert(m.Len() > 0, "Len should be > 0")
count := 1
for node := m.first; node.next != nil && count < 1000; node = node.next {
if node.prev != nil {
assert(node.prev.next == node, "Broken link between nodes")
}
count++
}
assert(count == m.Len(), fmt.Sprintf("Len expected %d, got %d", count, m.Len()))
}
}

View File

@ -0,0 +1,130 @@
package collections
import (
"github.com/stretchr/testify/assert"
"os"
"testing"
)
func TestMain(m *testing.M) {
checkStatus = true
exitCode := m.Run()
checkStatus = false
os.Exit(exitCode)
}
func contentCheck(t *testing.T, m *LinkedMap[string, int],
keys []string, values []int) {
assert.True(t, len(keys) == len(values), "input error expected keys and values differ in length")
// keys
i := 0
for key := range m.RangeKeys() {
assert.True(t, i < len(keys), "Too many elements in map")
assert.Equal(t, keys[i], key)
i++
}
assert.Equal(t, len(keys), i)
// values
i = 0
for value := range m.RangeValues() {
assert.True(t, i < len(values), "Too many elements in map")
assert.Equal(t, values[i], value)
i++
}
assert.Equal(t, len(values), i)
// Entries
i = 0
for entry := range m.RangeEntries() {
assert.True(t, i < len(values), "Too many elements in map")
assert.Equal(t, keys[i], entry.Key)
assert.Equal(t, values[i], entry.Value)
i++
}
assert.Equal(t, len(values), i)
// Get and Contains
for i, key := range keys {
v, ok := m.Get(key)
assert.True(t, ok)
assert.Equal(t, values[i], v)
assert.True(t, m.Contains(key))
}
}
func Test_emptymap(t *testing.T) {
m := NewLinkedMap[string, int]()
contentCheck(t, m, []string{}, []int{})
}
func Test_elementAddRemove(t *testing.T) {
m := NewLinkedMap[string, int]()
m.Put("a", 1)
contentCheck(t, m, []string{"a"}, []int{1})
assert.False(t, m.Delete("b"))
contentCheck(t, m, []string{"a"}, []int{1})
assert.True(t, m.Delete("a"))
contentCheck(t, m, []string{}, []int{})
}
func Test_GetContainsForElementsNotInMap(t *testing.T) {
m := createSimpleMap(t)
assert.False(t, m.Contains("d"))
val, ok := m.Get("d")
assert.False(t, ok)
assert.Equal(t, 0, val)
}
func Test_elementRemoveBeginning(t *testing.T) {
m := createSimpleMap(t)
assert.True(t, m.Delete("a"))
contentCheck(t, m, []string{"b", "c"}, []int{2, 3})
}
func Test_elementRemoveMiddle(t *testing.T) {
m := createSimpleMap(t)
assert.True(t, m.Delete("b"))
contentCheck(t, m, []string{"a", "c"}, []int{1, 3})
}
func Test_elementRemoveEnd(t *testing.T) {
m := createSimpleMap(t)
assert.True(t, m.Delete("c"))
contentCheck(t, m, []string{"a", "b"}, []int{1, 2})
}
func Test_addSameElementAgain(t *testing.T) {
m := createSimpleMap(t)
m.Put("b", 4)
contentCheck(t, m, []string{"a", "c", "b"}, []int{1, 3, 4})
}
func createSimpleMap(t *testing.T) *LinkedMap[string, int] {
m := NewLinkedMap[string, int]()
m.Put("a", 1)
m.Put("b", 2)
m.Put("c", 3)
contentCheck(t, m, []string{"a", "b", "c"}, []int{1, 2, 3})
return m
}
func Test_manyElements(t *testing.T) {
m := NewLinkedMap[string, int]()
chars := "0123456789"
for i := 0; i < 10000; i++ {
m.Put(chars[i%10:i%10+1], i)
}
contentCheck(t, m,
[]string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"},
[]int{9990, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999})
}