Skip to content

Commit

Permalink
Go/Web: Show number of unknown devices (#54)
Browse files Browse the repository at this point in the history
* Show number of unknown devices at Home page

* Add /api/v1/status endpoint

* Add Status and StatusTx storage intefaces
  • Loading branch information
thinkofher committed May 15, 2021
2 parents ad963dd + b146158 commit 818166c
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 24 deletions.
7 changes: 6 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ func main() {
}

ctx := context.Background()
macChannel, macDeamon := status.NewDaemon(ctx, factoryStorage.StatusIterator())
macChannel, macDeamon := status.NewDaemon(
ctx,
factoryStorage.StatusIterator(),
factoryStorage.StatusTx(),
)

// CORS (Cross-Origin Resource Sharing) middleware that enables public
// access to GET/OPTIONS requests. Used to expose APIs to XHR consumers in
Expand Down Expand Up @@ -120,6 +124,7 @@ func main() {
"/update",
api.UpdateStatus(macChannel),
)
r.Get("/status", api.Status(factoryStorage.StatusTx()))
})

r.With(lsmiddleware.ApiAuth(*config, false)).Get("/who", handlers.Who())
Expand Down
35 changes: 35 additions & 0 deletions pkg/services/handlers/api/v1/api.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"context"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -285,6 +286,40 @@ func UpdateStatus(ch chan<- []net.HardwareAddr) http.HandlerFunc {
}
}

func Status(counters storage.StatusTx) http.HandlerFunc {
var response struct {
Online int `json:"online"`
Unknown int `json:"unknown"`
}

return func(w http.ResponseWriter, r *http.Request) {
err := counters.DevicesStatus(
r.Context(),
func(ctx context.Context, s storage.Status) error {
online, err := s.OnlineUsers(ctx)
if err != nil {
return fmt.Errorf("failed to read online users: %w", err)
}

unknown, err := s.UnknownDevices(ctx)
if err != nil {
return fmt.Errorf("failed to read unknown devices: %w", err)
}

response.Online = online
response.Unknown = unknown
return nil
},
)
if err != nil {
internalServerError(w)
return
}

gores.JSONIndent(w, http.StatusOK, response, defaultPrefix, defaultIndent)
}
}

func internalServerError(w http.ResponseWriter) {
result.JSONError(w, &result.JSONErrorBody{
Message: "ooops! things are not going that great after all",
Expand Down
10 changes: 8 additions & 2 deletions pkg/services/status/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ type Daemon func()

// NewDeamon returns channel for communicating with deamon and deamon
// to be run in the background in the separate gourtine.
func NewDaemon(ctx context.Context, iter storage.StatusIterator) (chan<- []net.HardwareAddr, Daemon) {
func NewDaemon(ctx context.Context,
iter storage.StatusIterator, counters storage.StatusTx,
) (chan<- []net.HardwareAddr, Daemon) {
ch := make(chan []net.HardwareAddr)

daemon := func() {
Expand All @@ -33,7 +35,11 @@ func NewDaemon(ctx context.Context, iter storage.StatusIterator) (chan<- []net.H
macs = newMacs
case <-ticker.C: // Update users every minute with newest mac addresses
// Update online status for every user in db
err := storage.UpdateStatuses(ctx, macs, iter)
err := storage.UpdateStatuses(ctx, storage.UpdateStatusesArgs{
Addresses: macs,
Iter: iter,
Counters: counters,
})
if err != nil {
log.Println("Failed to update statuses, reason: ", err.Error())
continue
Expand Down
119 changes: 113 additions & 6 deletions pkg/storage/memory/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
bolt "go.etcd.io/bbolt"

"github.com/hakierspejs/long-season/pkg/models"
"github.com/hakierspejs/long-season/pkg/storage"
serrors "github.com/hakierspejs/long-season/pkg/storage/errors"
)

Expand All @@ -25,9 +26,10 @@ const (
// Factory implements storage.Factory interface for
// bolt database.
type Factory struct {
users *UsersStorage
devices *DevicesStorage
statusIter *StatusIterator
users *UsersStorage
devices *DevicesStorage
statusIter *StatusIterator
statusStorageTx *StatusStorageTx
}

// Users returns storage interface for manipulating
Expand All @@ -46,6 +48,13 @@ func (f Factory) StatusIterator() *StatusIterator {
return f.statusIter
}

// StatusTx returns storage interface for
// reading and writing information about numbers
// of online users and unkown devices.
func (f Factory) StatusTx() *StatusStorageTx {
return f.statusStorageTx
}

// New returns pointer to new memory storage
// Factory.
func New(db *bolt.DB) (*Factory, error) {
Expand All @@ -68,9 +77,10 @@ func New(db *bolt.DB) (*Factory, error) {
}

return &Factory{
users: &UsersStorage{db},
devices: &DevicesStorage{db},
statusIter: &StatusIterator{db},
users: &UsersStorage{db},
devices: &DevicesStorage{db},
statusIter: &StatusIterator{db},
statusStorageTx: &StatusStorageTx{db},
}, nil
}

Expand Down Expand Up @@ -709,3 +719,100 @@ func (s *StatusIterator) ForEachUpdate(
})
})
}

const (
onlineUsersCounter = "ls::users::online::counter"
unknownDevicesCounter = "ls::devices::unknown::counter"
)

// status implements storage.Status interface.
//
// status is able to perform multiple operation, but only in
// single transaction, because it holds pointer to bolt.Tx instead
// of pointer to bolt.DB as other types in this package.
type status struct {
tx *bolt.Tx
}

// OnlineUsers returns number of people being currently online.
func (s *status) OnlineUsers(ctx context.Context) (int, error) {
b, err := s.tx.CreateBucketIfNotExists([]byte(countersBucket))
if err != nil {
return 0, fmt.Errorf("failed to create %s bucket: %w", countersBucket, err)
}

onlineUsers := b.Get([]byte(onlineUsersCounter))
if onlineUsers == nil {
return 0, fmt.Errorf("failed to retrieve online users counter")
}

parsedOnlineUsers, err := strconv.Atoi(string(onlineUsers))
if err != nil {
return 0, fmt.Errorf(
"failed to parse slice bytes: %s into integer: %w",
onlineUsers, err,
)
}

return parsedOnlineUsers, nil
}

// SetOnlineUsers ovewrites number of people being currently online.
func (s *status) SetOnlineUsers(ctx context.Context, number int) error {
b, err := s.tx.CreateBucketIfNotExists([]byte(countersBucket))
if err != nil {
return fmt.Errorf("failed to create %s bucket: %w", countersBucket, err)
}

return b.Put([]byte(onlineUsersCounter), []byte(strconv.Itoa(number)))
}

// UnknownDevices returns number of unknown devices connected to the network.
func (s *status) UnknownDevices(ctx context.Context) (int, error) {
b, err := s.tx.CreateBucketIfNotExists([]byte(countersBucket))
if err != nil {
return 0, fmt.Errorf("failed to create %s bucket: %w", countersBucket, err)
}

unknownDevices := b.Get([]byte(unknownDevicesCounter))
if unknownDevices == nil {
return 0, fmt.Errorf("failed to retrieve unknown devices counter")
}

parsedUnknownDevices, err := strconv.Atoi(string(unknownDevices))
if err != nil {
return 0, fmt.Errorf(
"failed to parse slice bytes: %s into integer: %w",
unknownDevices, err,
)
}

return parsedUnknownDevices, nil
}

// SetUnknownDevices overwrites number of unknown devices connected to the network.
func (s *status) SetUnknownDevices(ctx context.Context, number int) error {
b, err := s.tx.CreateBucketIfNotExists([]byte(countersBucket))
if err != nil {
return fmt.Errorf("failed to create %s bucket: %w", countersBucket, err)
}

return b.Put([]byte(unknownDevicesCounter), []byte(strconv.Itoa(number)))
}

// StatusStorageTx implements storage.StatusTx interface.
type StatusStorageTx struct {
db *bolt.DB
}

// DevicesStatus accepts function that manipulates number of
// unknown devices and online users in single safe transaction.
func (s *StatusStorageTx) DevicesStatus(
ctx context.Context,
f func(context.Context, storage.Status) error,
) error {
return s.db.Update(func(tx *bolt.Tx) error {
statusStorage := &status{tx}
return f(ctx, statusStorage)
})
}
36 changes: 31 additions & 5 deletions pkg/storage/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package storage

import (
"context"
"fmt"
"net"

"github.com/hakierspejs/long-season/pkg/models"
Expand All @@ -18,20 +19,27 @@ type StatusIterator interface {
) error
}

// UpdateStatusesArgs contains arguments for UpdateStatuses function.
type UpdateStatusesArgs struct {
Addresses []net.HardwareAddr
Iter StatusIterator
Counters StatusTx
}

// UpdateStatuses set online user fields, with any device's MAC equal to one
// of addresses from given slice, to true and writes them to database.
func UpdateStatuses(
ctx context.Context, addresses []net.HardwareAddr, iter StatusIterator,
) error {
func UpdateStatuses(ctx context.Context, args UpdateStatusesArgs) error {

return iter.ForEachUpdate(ctx,
known, unknown := 0, 0
err := args.Iter.ForEachUpdate(ctx,
func(u models.User, devices []models.Device) (*models.User, error) {
result := u
result.Online = false

for _, address := range addresses {
for _, address := range args.Addresses {
for _, device := range devices {
if err := bcrypt.CompareHashAndPassword(device.MAC, address); err == nil {
known += 1
result.Online = true
return &result, nil
}
Expand All @@ -40,4 +48,22 @@ func UpdateStatuses(

return &result, nil
})
if err != nil {
return fmt.Errorf("failed to update statuses: %w", err)
}

unknown = len(args.Addresses) - known

return args.Counters.DevicesStatus(ctx,
func(ctx context.Context, s Status) error {
if err := s.SetOnlineUsers(ctx, known); err != nil {
return fmt.Errorf("failed to set online users: %w", err)
}

if err := s.SetUnknownDevices(ctx, unknown); err != nil {
return fmt.Errorf("failed to set unknown devices: %w", err)
}

return nil
})
}
32 changes: 32 additions & 0 deletions pkg/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,35 @@ type Devices interface {
Update(ctx context.Context, d models.Device) error
Remove(ctx context.Context, id int) error
}

// Status interface provides methods for reading and
// writing numerical information about users and devices
// spending time in hackerspace.
type Status interface {
// OnlineUsers returns number of people being
// currently online.
OnlineUsers(ctx context.Context) (int, error)

// SetOnlineUsers ovewrites number of people being
// currently online.
SetOnlineUsers(ctx context.Context, number int) error

// UnknownDevices returns number of unknown devices
// connected to the network.
UnknownDevices(ctx context.Context) (int, error)

// SetUnknownDevices overwrites number of unknown devices
// connected to the network.
SetUnknownDevices(ctx context.Context, number int) error
}

// StatusTx interface provides methods for reading and
// writing numerical information about users and devices
// spending time in hackerspace in one safe transaction.
//
// Use this interface if you want to omit data races.
type StatusTx interface {
// DevicesStatus accepts function that manipulates number of
// unknown devices and online users in single safe transaction.
DevicesStatus(context.Context, func(context.Context, Status) error) error
}
Loading

0 comments on commit 818166c

Please sign in to comment.