clone
This commit is contained in:
266
server/persistence/bolt/bolt.go
Normal file
266
server/persistence/bolt/bolt.go
Normal file
@ -0,0 +1,266 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
sgob "github.com/asdine/storm/codec/gob"
|
||||
"github.com/asdine/storm/v3"
|
||||
"go.etcd.io/bbolt"
|
||||
|
||||
"github.com/mochi-co/mqtt/server/persistence"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
// defaultPath is the default file to use to store the data.
|
||||
defaultPath = "mochi.db"
|
||||
|
||||
// defaultTimeout is the default timeout of the file lock.
|
||||
defaultTimeout = 250 * time.Millisecond
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrDBNotOpen indicates the bolt db file is not open for reading.
|
||||
ErrDBNotOpen = fmt.Errorf("boltdb not opened")
|
||||
)
|
||||
|
||||
// Store is a backend for writing and reading to bolt persistent storage.
|
||||
type Store struct {
|
||||
path string // the path on which to store the db file.
|
||||
opts *bbolt.Options // options for configuring the boltdb instance.
|
||||
db *storm.DB // the boltdb instance.
|
||||
}
|
||||
|
||||
// New returns a configured instance of the boltdb store.
|
||||
func New(path string, opts *bbolt.Options) *Store {
|
||||
if path == "" || path == "." {
|
||||
path = defaultPath
|
||||
}
|
||||
|
||||
if opts == nil {
|
||||
opts = &bbolt.Options{
|
||||
Timeout: defaultTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
return &Store{
|
||||
path: path,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
// Open opens the boltdb instance.
|
||||
func (s *Store) Open() error {
|
||||
var err error
|
||||
s.db, err = storm.Open(s.path, storm.BoltOptions(0600, s.opts), storm.Codec(sgob.Codec))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the boltdb instance.
|
||||
func (s *Store) Close() {
|
||||
s.db.Close()
|
||||
}
|
||||
|
||||
// WriteServerInfo writes the server info to the boltdb instance.
|
||||
func (s *Store) WriteServerInfo(v persistence.ServerInfo) error {
|
||||
if s.db == nil {
|
||||
return ErrDBNotOpen
|
||||
}
|
||||
|
||||
err := s.db.Save(&v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteSubscription writes a single subscription to the boltdb instance.
|
||||
func (s *Store) WriteSubscription(v persistence.Subscription) error {
|
||||
if s.db == nil {
|
||||
return ErrDBNotOpen
|
||||
}
|
||||
|
||||
err := s.db.Save(&v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteInflight writes a single inflight message to the boltdb instance.
|
||||
func (s *Store) WriteInflight(v persistence.Message) error {
|
||||
if s.db == nil {
|
||||
return ErrDBNotOpen
|
||||
}
|
||||
|
||||
err := s.db.Save(&v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteRetained writes a single retained message to the boltdb instance.
|
||||
func (s *Store) WriteRetained(v persistence.Message) error {
|
||||
if s.db == nil {
|
||||
return ErrDBNotOpen
|
||||
}
|
||||
|
||||
err := s.db.Save(&v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteClient writes a single client to the boltdb instance.
|
||||
func (s *Store) WriteClient(v persistence.Client) error {
|
||||
if s.db == nil {
|
||||
return ErrDBNotOpen
|
||||
}
|
||||
|
||||
err := s.db.Save(&v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSubscription deletes a subscription from the boltdb instance.
|
||||
func (s *Store) DeleteSubscription(id string) error {
|
||||
if s.db == nil {
|
||||
return ErrDBNotOpen
|
||||
}
|
||||
|
||||
err := s.db.DeleteStruct(&persistence.Subscription{
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteClient deletes a client from the boltdb instance.
|
||||
func (s *Store) DeleteClient(id string) error {
|
||||
if s.db == nil {
|
||||
return ErrDBNotOpen
|
||||
}
|
||||
|
||||
err := s.db.DeleteStruct(&persistence.Client{
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteInflight deletes an inflight message from the boltdb instance.
|
||||
func (s *Store) DeleteInflight(id string) error {
|
||||
if s.db == nil {
|
||||
return ErrDBNotOpen
|
||||
}
|
||||
|
||||
err := s.db.DeleteStruct(&persistence.Message{
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRetained deletes a retained message from the boltdb instance.
|
||||
func (s *Store) DeleteRetained(id string) error {
|
||||
if s.db == nil {
|
||||
return ErrDBNotOpen
|
||||
}
|
||||
|
||||
err := s.db.DeleteStruct(&persistence.Message{
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadSubscriptions loads all the subscriptions from the boltdb instance.
|
||||
func (s *Store) ReadSubscriptions() (v []persistence.Subscription, err error) {
|
||||
if s.db == nil {
|
||||
return v, ErrDBNotOpen
|
||||
}
|
||||
|
||||
err = s.db.Find("T", persistence.KSubscription, &v)
|
||||
if err != nil && err != storm.ErrNotFound {
|
||||
return
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// ReadClients loads all the clients from the boltdb instance.
|
||||
func (s *Store) ReadClients() (v []persistence.Client, err error) {
|
||||
if s.db == nil {
|
||||
return v, ErrDBNotOpen
|
||||
}
|
||||
|
||||
err = s.db.Find("T", persistence.KClient, &v)
|
||||
if err != nil && err != storm.ErrNotFound {
|
||||
return
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// ReadInflight loads all the inflight messages from the boltdb instance.
|
||||
func (s *Store) ReadInflight() (v []persistence.Message, err error) {
|
||||
if s.db == nil {
|
||||
return v, ErrDBNotOpen
|
||||
}
|
||||
|
||||
err = s.db.Find("T", persistence.KInflight, &v)
|
||||
if err != nil && err != storm.ErrNotFound {
|
||||
return
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// ReadRetained loads all the retained messages from the boltdb instance.
|
||||
func (s *Store) ReadRetained() (v []persistence.Message, err error) {
|
||||
if s.db == nil {
|
||||
return v, ErrDBNotOpen
|
||||
}
|
||||
|
||||
err = s.db.Find("T", persistence.KRetained, &v)
|
||||
if err != nil && err != storm.ErrNotFound {
|
||||
return
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
//ReadServerInfo loads the server info from the boltdb instance.
|
||||
func (s *Store) ReadServerInfo() (v persistence.ServerInfo, err error) {
|
||||
if s.db == nil {
|
||||
return v, ErrDBNotOpen
|
||||
}
|
||||
|
||||
err = s.db.One("ID", persistence.KServerInfo, &v)
|
||||
if err != nil && err != storm.ErrNotFound {
|
||||
return
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
486
server/persistence/bolt/bolt_test.go
Normal file
486
server/persistence/bolt/bolt_test.go
Normal file
@ -0,0 +1,486 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.etcd.io/bbolt"
|
||||
|
||||
"github.com/mochi-co/mqtt/server/persistence"
|
||||
"github.com/mochi-co/mqtt/server/system"
|
||||
)
|
||||
|
||||
const tmpPath = "testbolt.db"
|
||||
|
||||
func teardown(s *Store, t *testing.T) {
|
||||
s.Close()
|
||||
err := os.Remove(tmpPath)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSatsifies(t *testing.T) {
|
||||
var x persistence.Store
|
||||
x = New(tmpPath, &bbolt.Options{
|
||||
Timeout: 500 * time.Millisecond,
|
||||
})
|
||||
require.NotNil(t, x)
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
s := New(tmpPath, &bbolt.Options{
|
||||
Timeout: 500 * time.Millisecond,
|
||||
})
|
||||
require.NotNil(t, s)
|
||||
require.Equal(t, tmpPath, s.path)
|
||||
require.Equal(t, 500*time.Millisecond, s.opts.Timeout)
|
||||
}
|
||||
|
||||
func TestNewNoPath(t *testing.T) {
|
||||
s := New("", nil)
|
||||
require.NotNil(t, s)
|
||||
require.Equal(t, defaultPath, s.path)
|
||||
}
|
||||
|
||||
func TestNewNoOpts(t *testing.T) {
|
||||
s := New("", nil)
|
||||
require.NotNil(t, s)
|
||||
require.Equal(t, defaultTimeout, s.opts.Timeout)
|
||||
}
|
||||
|
||||
func TestOpen(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
defer teardown(s, t)
|
||||
require.NotNil(t, s.db)
|
||||
}
|
||||
|
||||
func TestOpenFailure(t *testing.T) {
|
||||
s := New("..", nil)
|
||||
err := s.Open()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestWriteAndRetrieveServerInfo(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
defer teardown(s, t)
|
||||
|
||||
v := system.Info{
|
||||
Version: "test",
|
||||
Started: 100,
|
||||
}
|
||||
err = s.WriteServerInfo(persistence.ServerInfo{
|
||||
Info: v,
|
||||
ID: persistence.KServerInfo,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
r, err := s.ReadServerInfo()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r)
|
||||
require.Equal(t, v.Version, r.Version)
|
||||
require.Equal(t, v.Started, r.Started)
|
||||
}
|
||||
|
||||
func TestWriteServerInfoNoDB(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.WriteServerInfo(persistence.ServerInfo{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestWriteServerInfoFail(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.Remove(tmpPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.WriteServerInfo(persistence.ServerInfo{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestReadServerInfoNoDB(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
_, err := s.ReadServerInfo()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestReadServerInfoFail(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
s.Close()
|
||||
_, err = s.ReadServerInfo()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestWriteRetrieveDeleteSubscription(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
defer teardown(s, t)
|
||||
|
||||
v := persistence.Subscription{
|
||||
ID: "test:a/b/c",
|
||||
Client: "test",
|
||||
Filter: "a/b/c",
|
||||
QoS: 1,
|
||||
T: persistence.KSubscription,
|
||||
}
|
||||
err = s.WriteSubscription(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
v2 := persistence.Subscription{
|
||||
ID: "test:d/e/f",
|
||||
Client: "test",
|
||||
Filter: "d/e/f",
|
||||
QoS: 2,
|
||||
T: persistence.KSubscription,
|
||||
}
|
||||
err = s.WriteSubscription(v2)
|
||||
require.NoError(t, err)
|
||||
|
||||
subs, err := s.ReadSubscriptions()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, persistence.KSubscription, subs[0].T)
|
||||
require.Equal(t, 2, len(subs))
|
||||
|
||||
err = s.DeleteSubscription("test:d/e/f")
|
||||
require.NoError(t, err)
|
||||
|
||||
subs, err = s.ReadSubscriptions()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(subs))
|
||||
}
|
||||
|
||||
func TestWriteSubscriptionNoDB(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.WriteSubscription(persistence.Subscription{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestWriteSubscriptionFail(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
s.Close()
|
||||
err = s.WriteSubscription(persistence.Subscription{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestReadSubscriptionNoDB(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
_, err := s.ReadSubscriptions()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestReadSubscriptionFail(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
s.Close()
|
||||
_, err = s.ReadSubscriptions()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestWriteRetrieveDeleteInflight(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
defer teardown(s, t)
|
||||
|
||||
v := persistence.Message{
|
||||
ID: "client1_if_0",
|
||||
T: persistence.KInflight,
|
||||
PacketID: 0,
|
||||
TopicName: "a/b/c",
|
||||
Payload: []byte{'h', 'e', 'l', 'l', 'o'},
|
||||
Sent: 100,
|
||||
Resends: 0,
|
||||
}
|
||||
err = s.WriteInflight(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
v2 := persistence.Message{
|
||||
ID: "client1_if_100",
|
||||
T: persistence.KInflight,
|
||||
PacketID: 100,
|
||||
TopicName: "d/e/f",
|
||||
Payload: []byte{'y', 'e', 's'},
|
||||
Sent: 200,
|
||||
Resends: 1,
|
||||
}
|
||||
err = s.WriteInflight(v2)
|
||||
require.NoError(t, err)
|
||||
|
||||
msgs, err := s.ReadInflight()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, persistence.KInflight, msgs[0].T)
|
||||
require.Equal(t, 2, len(msgs))
|
||||
|
||||
err = s.DeleteInflight("client1_if_100")
|
||||
require.NoError(t, err)
|
||||
|
||||
msgs, err = s.ReadInflight()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(msgs))
|
||||
|
||||
}
|
||||
|
||||
func TestWriteInflightNoDB(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.WriteInflight(persistence.Message{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestWriteInflightFail(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
s.Close()
|
||||
err = s.WriteInflight(persistence.Message{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestReadInflightNoDB(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
_, err := s.ReadInflight()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestReadInflightFail(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
s.Close()
|
||||
_, err = s.ReadInflight()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestWriteRetrieveDeleteRetained(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
defer teardown(s, t)
|
||||
|
||||
v := persistence.Message{
|
||||
ID: "client1_ret_200",
|
||||
T: persistence.KRetained,
|
||||
FixedHeader: persistence.FixedHeader{
|
||||
Retain: true,
|
||||
},
|
||||
PacketID: 200,
|
||||
TopicName: "a/b/c",
|
||||
Payload: []byte{'h', 'e', 'l', 'l', 'o'},
|
||||
Sent: 100,
|
||||
Resends: 0,
|
||||
}
|
||||
err = s.WriteRetained(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
v2 := persistence.Message{
|
||||
ID: "client1_ret_300",
|
||||
T: persistence.KRetained,
|
||||
FixedHeader: persistence.FixedHeader{
|
||||
Retain: true,
|
||||
},
|
||||
PacketID: 100,
|
||||
TopicName: "d/e/f",
|
||||
Payload: []byte{'y', 'e', 's'},
|
||||
Sent: 200,
|
||||
Resends: 1,
|
||||
}
|
||||
err = s.WriteRetained(v2)
|
||||
require.NoError(t, err)
|
||||
|
||||
msgs, err := s.ReadRetained()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, persistence.KRetained, msgs[0].T)
|
||||
require.Equal(t, true, msgs[0].FixedHeader.Retain)
|
||||
require.Equal(t, 2, len(msgs))
|
||||
|
||||
err = s.DeleteRetained("client1_ret_300")
|
||||
require.NoError(t, err)
|
||||
|
||||
msgs, err = s.ReadRetained()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(msgs))
|
||||
}
|
||||
|
||||
func TestWriteRetainedNoDB(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.WriteRetained(persistence.Message{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestWriteRetainedFail(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.Remove(tmpPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.WriteRetained(persistence.Message{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestReadRetainedNoDB(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
_, err := s.ReadRetained()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestReadRetainedFail(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
s.Close()
|
||||
_, err = s.ReadRetained()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestWriteRetrieveDeleteClients(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
defer teardown(s, t)
|
||||
|
||||
v := persistence.Client{
|
||||
ID: "cl_client1",
|
||||
ClientID: "client1",
|
||||
T: persistence.KClient,
|
||||
Listener: "tcp1",
|
||||
Username: []byte{'m', 'o', 'c', 'h', 'i'},
|
||||
LWT: persistence.LWT{
|
||||
Topic: "a/b/c",
|
||||
Message: []byte{'h', 'e', 'l', 'l', 'o'},
|
||||
Qos: 1,
|
||||
Retain: true,
|
||||
},
|
||||
}
|
||||
err = s.WriteClient(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
clients, err := s.ReadClients()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, []byte{'m', 'o', 'c', 'h', 'i'}, clients[0].Username)
|
||||
require.Equal(t, "a/b/c", clients[0].LWT.Topic)
|
||||
|
||||
v2 := persistence.Client{
|
||||
ID: "cl_client2",
|
||||
ClientID: "client2",
|
||||
T: persistence.KClient,
|
||||
Listener: "tcp1",
|
||||
}
|
||||
err = s.WriteClient(v2)
|
||||
require.NoError(t, err)
|
||||
|
||||
clients, err = s.ReadClients()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, persistence.KClient, clients[0].T)
|
||||
require.Equal(t, 2, len(clients))
|
||||
|
||||
err = s.DeleteClient("cl_client2")
|
||||
require.NoError(t, err)
|
||||
|
||||
clients, err = s.ReadClients()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(clients))
|
||||
}
|
||||
|
||||
func TestWriteClientNoDB(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.WriteClient(persistence.Client{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestWriteClientFail(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
s.Close()
|
||||
err = s.WriteClient(persistence.Client{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestReadClientNoDB(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
_, err := s.ReadClients()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestReadClientFail(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
s.Close()
|
||||
_, err = s.ReadClients()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteSubscriptionNoDB(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.DeleteSubscription("a")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteSubscriptionFail(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
s.Close()
|
||||
err = s.DeleteSubscription("a")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteClientNoDB(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.DeleteClient("a")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteClientFail(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
s.Close()
|
||||
err = s.DeleteClient("a")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteInflightNoDB(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.DeleteInflight("a")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteInflightFail(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
s.Close()
|
||||
err = s.DeleteInflight("a")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteRetainedNoDB(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.DeleteRetained("a")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteRetainedFail(t *testing.T) {
|
||||
s := New(tmpPath, nil)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
s.Close()
|
||||
err = s.DeleteRetained("a")
|
||||
require.Error(t, err)
|
||||
}
|
291
server/persistence/persistence.go
Normal file
291
server/persistence/persistence.go
Normal file
@ -0,0 +1,291 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/mochi-co/mqtt/server/system"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
// KSubscription is the key for subscription data.
|
||||
KSubscription = "sub"
|
||||
|
||||
// KServerInfo is the key for server info data.
|
||||
KServerInfo = "srv"
|
||||
|
||||
// KRetained is the key for retained messages data.
|
||||
KRetained = "ret"
|
||||
|
||||
// KInflight is the key for inflight messages data.
|
||||
KInflight = "ifm"
|
||||
|
||||
// KClient is the key for client data.
|
||||
KClient = "cl"
|
||||
)
|
||||
|
||||
// Store is an interface which details a persistent storage connector.
|
||||
type Store interface {
|
||||
Open() error
|
||||
Close()
|
||||
WriteSubscription(v Subscription) error
|
||||
WriteClient(v Client) error
|
||||
WriteInflight(v Message) error
|
||||
WriteServerInfo(v ServerInfo) error
|
||||
WriteRetained(v Message) error
|
||||
|
||||
DeleteSubscription(id string) error
|
||||
DeleteClient(id string) error
|
||||
DeleteInflight(id string) error
|
||||
DeleteRetained(id string) error
|
||||
|
||||
ReadSubscriptions() (v []Subscription, err error)
|
||||
ReadInflight() (v []Message, err error)
|
||||
ReadRetained() (v []Message, err error)
|
||||
ReadClients() (v []Client, err error)
|
||||
ReadServerInfo() (v ServerInfo, err error)
|
||||
}
|
||||
|
||||
// ServerInfo contains information and statistics about the server.
|
||||
type ServerInfo struct {
|
||||
system.Info // embed the system info struct.
|
||||
ID string // the storage key.
|
||||
}
|
||||
|
||||
// Subscription contains the details of a topic filter subscription.
|
||||
type Subscription struct {
|
||||
ID string // the storage key.
|
||||
T string // the type of the stored data.
|
||||
Client string // the id of the client who the subscription belongs to.
|
||||
Filter string // the topic filter being subscribed to.
|
||||
QoS byte // the desired QoS byte.
|
||||
}
|
||||
|
||||
// Message contains the details of a retained or inflight message.
|
||||
type Message struct {
|
||||
Payload []byte // the message payload (if retained).
|
||||
FixedHeader FixedHeader // the header properties of the message.
|
||||
T string // the type of the stored data.
|
||||
ID string // the storage key.
|
||||
Client string // the id of the client who sent the message (if inflight).
|
||||
TopicName string // the topic the message was sent to (if retained).
|
||||
Sent int64 // the last time the message was sent (for retries) in unixtime (if inflight).
|
||||
Resends int // the number of times the message was attempted to be sent (if inflight).
|
||||
PacketID uint16 // the unique id of the packet (if inflight).
|
||||
}
|
||||
|
||||
// FixedHeader contains the fixed header properties of a message.
|
||||
type FixedHeader struct {
|
||||
Remaining int // the number of remaining bytes in the payload.
|
||||
Type byte // the type of the packet (PUBLISH, SUBSCRIBE, etc) from bits 7 - 4 (byte 1).
|
||||
Qos byte // indicates the quality of service expected.
|
||||
Dup bool // indicates if the packet was already sent at an earlier time.
|
||||
Retain bool // whether the message should be retained.
|
||||
}
|
||||
|
||||
// Client contains client data that can be persistently stored.
|
||||
type Client struct {
|
||||
LWT LWT // the last-will-and-testament message for the client.
|
||||
Username []byte // the username the client authenticated with.
|
||||
ID string // the storage key.
|
||||
ClientID string // the id of the client.
|
||||
T string // the type of the stored data.
|
||||
Listener string // the last known listener id for the client
|
||||
}
|
||||
|
||||
// LWT contains details about a clients LWT payload.
|
||||
type LWT struct {
|
||||
Message []byte // the message that shall be sent when the client disconnects.
|
||||
Topic string // the topic the will message shall be sent to.
|
||||
Qos byte // the quality of service desired.
|
||||
Retain bool // indicates whether the will message should be retained
|
||||
}
|
||||
|
||||
// MockStore is a mock storage backend for testing.
|
||||
type MockStore struct {
|
||||
Fail map[string]bool // issue errors for different methods.
|
||||
FailOpen bool // error on open.
|
||||
Closed bool // indicate mock store is closed.
|
||||
Opened bool // indicate mock store is open.
|
||||
}
|
||||
|
||||
// Open opens the storage instance.
|
||||
func (s *MockStore) Open() error {
|
||||
if s.FailOpen {
|
||||
return errors.New("test")
|
||||
}
|
||||
|
||||
s.Opened = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the storage instance.
|
||||
func (s *MockStore) Close() {
|
||||
s.Closed = true
|
||||
}
|
||||
|
||||
// WriteSubscription writes a single subscription to the storage instance.
|
||||
func (s *MockStore) WriteSubscription(v Subscription) error {
|
||||
if _, ok := s.Fail["write_subs"]; ok {
|
||||
return errors.New("test")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteClient writes a single client to the storage instance.
|
||||
func (s *MockStore) WriteClient(v Client) error {
|
||||
if _, ok := s.Fail["write_clients"]; ok {
|
||||
return errors.New("test")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteInFlight writes a single InFlight message to the storage instance.
|
||||
func (s *MockStore) WriteInflight(v Message) error {
|
||||
if _, ok := s.Fail["write_inflight"]; ok {
|
||||
return errors.New("test")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteRetained writes a single retained message to the storage instance.
|
||||
func (s *MockStore) WriteRetained(v Message) error {
|
||||
if _, ok := s.Fail["write_retained"]; ok {
|
||||
return errors.New("test")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteServerInfo writes server info to the storage instance.
|
||||
func (s *MockStore) WriteServerInfo(v ServerInfo) error {
|
||||
if _, ok := s.Fail["write_info"]; ok {
|
||||
return errors.New("test")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSubscription deletes a subscription from the persistent store.
|
||||
func (s *MockStore) DeleteSubscription(id string) error {
|
||||
if _, ok := s.Fail["delete_subs"]; ok {
|
||||
return errors.New("test")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteClient deletes a client from the persistent store.
|
||||
func (s *MockStore) DeleteClient(id string) error {
|
||||
if _, ok := s.Fail["delete_clients"]; ok {
|
||||
return errors.New("test")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteInflight deletes an inflight message from the persistent store.
|
||||
func (s *MockStore) DeleteInflight(id string) error {
|
||||
if _, ok := s.Fail["delete_inflight"]; ok {
|
||||
return errors.New("test")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRetained deletes a retained message from the persistent store.
|
||||
func (s *MockStore) DeleteRetained(id string) error {
|
||||
if _, ok := s.Fail["delete_retained"]; ok {
|
||||
return errors.New("test")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadSubscriptions loads the subscriptions from the storage instance.
|
||||
func (s *MockStore) ReadSubscriptions() (v []Subscription, err error) {
|
||||
if _, ok := s.Fail["read_subs"]; ok {
|
||||
return v, errors.New("test_subs")
|
||||
}
|
||||
|
||||
return []Subscription{
|
||||
{
|
||||
ID: "test:a/b/c",
|
||||
Client: "test",
|
||||
Filter: "a/b/c",
|
||||
QoS: 1,
|
||||
T: KSubscription,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReadClients loads the clients from the storage instance.
|
||||
func (s *MockStore) ReadClients() (v []Client, err error) {
|
||||
if _, ok := s.Fail["read_clients"]; ok {
|
||||
return v, errors.New("test_clients")
|
||||
}
|
||||
|
||||
return []Client{
|
||||
{
|
||||
ID: "cl_client1",
|
||||
ClientID: "client1",
|
||||
T: KClient,
|
||||
Listener: "tcp1",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReadInflight loads the inflight messages from the storage instance.
|
||||
func (s *MockStore) ReadInflight() (v []Message, err error) {
|
||||
if _, ok := s.Fail["read_inflight"]; ok {
|
||||
return v, errors.New("test_inflight")
|
||||
}
|
||||
|
||||
return []Message{
|
||||
{
|
||||
ID: "client1_if_100",
|
||||
T: KInflight,
|
||||
Client: "client1",
|
||||
PacketID: 100,
|
||||
TopicName: "d/e/f",
|
||||
Payload: []byte{'y', 'e', 's'},
|
||||
Sent: 200,
|
||||
Resends: 1,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReadRetained loads the retained messages from the storage instance.
|
||||
func (s *MockStore) ReadRetained() (v []Message, err error) {
|
||||
if _, ok := s.Fail["read_retained"]; ok {
|
||||
return v, errors.New("test_retained")
|
||||
}
|
||||
|
||||
return []Message{
|
||||
{
|
||||
ID: "client1_ret_200",
|
||||
T: KRetained,
|
||||
FixedHeader: FixedHeader{
|
||||
Retain: true,
|
||||
},
|
||||
PacketID: 200,
|
||||
TopicName: "a/b/c",
|
||||
Payload: []byte{'h', 'e', 'l', 'l', 'o'},
|
||||
Sent: 100,
|
||||
Resends: 0,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
//ReadServerInfo loads the server info from the storage instance.
|
||||
func (s *MockStore) ReadServerInfo() (v ServerInfo, err error) {
|
||||
if _, ok := s.Fail["read_info"]; ok {
|
||||
return v, errors.New("test_info")
|
||||
}
|
||||
|
||||
return ServerInfo{
|
||||
system.Info{
|
||||
Version: "test",
|
||||
Started: 100,
|
||||
},
|
||||
KServerInfo,
|
||||
}, nil
|
||||
}
|
251
server/persistence/persistence_test.go
Normal file
251
server/persistence/persistence_test.go
Normal file
@ -0,0 +1,251 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMockStoreOpen(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
err := s.Open()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, s.Opened)
|
||||
}
|
||||
|
||||
func TestMockStoreOpenFail(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
s.FailOpen = true
|
||||
err := s.Open()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreClose(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
s.Close()
|
||||
require.Equal(t, true, s.Closed)
|
||||
}
|
||||
|
||||
func TestMockStoreWriteSubscription(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
err := s.WriteSubscription(Subscription{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreWriteSubscriptionFail(t *testing.T) {
|
||||
s := &MockStore{
|
||||
Fail: map[string]bool{
|
||||
"write_subs": true,
|
||||
},
|
||||
}
|
||||
err := s.WriteSubscription(Subscription{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreWriteClient(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
err := s.WriteClient(Client{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreWriteClientFail(t *testing.T) {
|
||||
s := &MockStore{
|
||||
Fail: map[string]bool{
|
||||
"write_clients": true,
|
||||
},
|
||||
}
|
||||
err := s.WriteClient(Client{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreWriteInflight(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
err := s.WriteInflight(Message{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreWriteInflightFail(t *testing.T) {
|
||||
s := &MockStore{
|
||||
Fail: map[string]bool{
|
||||
"write_inflight": true,
|
||||
},
|
||||
}
|
||||
err := s.WriteInflight(Message{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreWriteRetained(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
err := s.WriteRetained(Message{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreWriteRetainedFail(t *testing.T) {
|
||||
s := &MockStore{
|
||||
Fail: map[string]bool{
|
||||
"write_retained": true,
|
||||
},
|
||||
}
|
||||
err := s.WriteRetained(Message{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreWriteServerInfo(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
err := s.WriteServerInfo(ServerInfo{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreWriteServerInfoFail(t *testing.T) {
|
||||
s := &MockStore{
|
||||
Fail: map[string]bool{
|
||||
"write_info": true,
|
||||
},
|
||||
}
|
||||
err := s.WriteServerInfo(ServerInfo{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreDeleteSubscription(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
err := s.DeleteSubscription("client1:d/e/f")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreDeleteSubscriptionFail(t *testing.T) {
|
||||
s := &MockStore{
|
||||
Fail: map[string]bool{
|
||||
"delete_subs": true,
|
||||
},
|
||||
}
|
||||
err := s.DeleteSubscription("client1:a/b/c")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreDeleteClient(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
err := s.DeleteClient("client1")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreDeleteClientFail(t *testing.T) {
|
||||
s := &MockStore{
|
||||
Fail: map[string]bool{
|
||||
"delete_clients": true,
|
||||
},
|
||||
}
|
||||
err := s.DeleteClient("client1")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreDeleteInflight(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
err := s.DeleteInflight("client1-if-100")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreDeleteInflightFail(t *testing.T) {
|
||||
s := &MockStore{
|
||||
Fail: map[string]bool{
|
||||
"delete_inflight": true,
|
||||
},
|
||||
}
|
||||
err := s.DeleteInflight("client1-if-100")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreDeleteRetained(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
err := s.DeleteRetained("client1-ret-100")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreDeleteRetainedFail(t *testing.T) {
|
||||
s := &MockStore{
|
||||
Fail: map[string]bool{
|
||||
"delete_retained": true,
|
||||
},
|
||||
}
|
||||
err := s.DeleteRetained("client1-ret-100")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockStorReadServerInfo(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
_, err := s.ReadServerInfo()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockStorReadServerInfoFail(t *testing.T) {
|
||||
s := &MockStore{
|
||||
Fail: map[string]bool{
|
||||
"read_info": true,
|
||||
},
|
||||
}
|
||||
_, err := s.ReadServerInfo()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreReadSubscriptions(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
_, err := s.ReadSubscriptions()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreReadSubscriptionsFail(t *testing.T) {
|
||||
s := &MockStore{
|
||||
Fail: map[string]bool{
|
||||
"read_subs": true,
|
||||
},
|
||||
}
|
||||
_, err := s.ReadSubscriptions()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreReadClients(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
_, err := s.ReadClients()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreReadClientsFail(t *testing.T) {
|
||||
s := &MockStore{
|
||||
Fail: map[string]bool{
|
||||
"read_clients": true,
|
||||
},
|
||||
}
|
||||
_, err := s.ReadClients()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreReadInflight(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
_, err := s.ReadInflight()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreReadInflightFail(t *testing.T) {
|
||||
s := &MockStore{
|
||||
Fail: map[string]bool{
|
||||
"read_inflight": true,
|
||||
},
|
||||
}
|
||||
_, err := s.ReadInflight()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreReadRetained(t *testing.T) {
|
||||
s := new(MockStore)
|
||||
_, err := s.ReadRetained()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockStoreReadRetainedFail(t *testing.T) {
|
||||
s := &MockStore{
|
||||
Fail: map[string]bool{
|
||||
"read_retained": true,
|
||||
},
|
||||
}
|
||||
_, err := s.ReadRetained()
|
||||
require.Error(t, err)
|
||||
}
|
Reference in New Issue
Block a user