Initial QSfera import
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
ehmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/eventhistory/v0"
|
||||
ehsvc "github.com/qsfera/server/protogen/gen/qsfera/services/eventhistory/v0"
|
||||
"github.com/qsfera/server/services/eventhistory/pkg/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// StoreEvent is data structure in the store
|
||||
type StoreEvent struct {
|
||||
ID string
|
||||
Type string
|
||||
Event []byte
|
||||
}
|
||||
|
||||
// EventHistoryService is the service responsible for event history
|
||||
type EventHistoryService struct {
|
||||
ch <-chan events.Event
|
||||
store store.Store
|
||||
cfg *config.Config
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// NewEventHistoryService returns an EventHistory service
|
||||
func NewEventHistoryService(cfg *config.Config, consumer events.Consumer, store store.Store, log log.Logger) (*EventHistoryService, error) {
|
||||
if consumer == nil || store == nil {
|
||||
return nil, fmt.Errorf("need non nil consumer (%v) and store (%v) to work properly", consumer, store)
|
||||
}
|
||||
|
||||
ch, err := events.ConsumeAll(consumer, "evhistory")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eh := &EventHistoryService{ch: ch, store: store, cfg: cfg, log: log}
|
||||
go eh.StoreEvents()
|
||||
|
||||
return eh, nil
|
||||
}
|
||||
|
||||
// StoreEvents consumes all events and stores them in the store. Will block
|
||||
func (eh *EventHistoryService) StoreEvents() {
|
||||
for event := range eh.ch {
|
||||
ev, err := json.Marshal(StoreEvent{
|
||||
ID: event.ID,
|
||||
Type: event.Type,
|
||||
Event: event.Event.([]byte),
|
||||
})
|
||||
if err != nil {
|
||||
eh.log.Error().Err(err).Str("eventid", event.ID).Msg("could not marshal event")
|
||||
continue
|
||||
}
|
||||
if err := eh.store.Write(&store.Record{
|
||||
Key: event.ID,
|
||||
Value: ev,
|
||||
Expiry: eh.cfg.Store.TTL,
|
||||
Metadata: map[string]any{
|
||||
"type": event.Type,
|
||||
},
|
||||
}); err != nil {
|
||||
// we can't store. That's it for us.
|
||||
eh.log.Error().Err(err).Str("eventid", event.ID).Msg("could not store event")
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetEvents allows retrieving events from the eventstore by id
|
||||
func (eh *EventHistoryService) GetEvents(ctx context.Context, req *ehsvc.GetEventsRequest, resp *ehsvc.GetEventsResponse) error {
|
||||
for _, id := range req.Ids {
|
||||
ev, err := eh.getEvent(id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
resp.Events = append(resp.Events, ev)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEventsForUser allows retrieving events from the eventstore by userID
|
||||
// This function will match all events that contains the user ID between two non-word characters.
|
||||
// The reasoning behind this is that events put the userID in many different fields, which can differ
|
||||
// per event type. This function will match all events that contain the userID by using a regex.
|
||||
// This should also cover future events that might contain the userID in a different field.
|
||||
func (eh *EventHistoryService) GetEventsForUser(ctx context.Context, req *ehsvc.GetEventsForUserRequest, resp *ehsvc.GetEventsResponse) error {
|
||||
idx, err := eh.store.List(store.ListPrefix(""))
|
||||
if err != nil {
|
||||
eh.log.Error().Err(err).Msg("could not list events")
|
||||
return err
|
||||
}
|
||||
|
||||
// Match all events that contains the user ID between two non-word characters.
|
||||
userID, err := regexp.Compile(fmt.Sprintf(`\W%s\W`, req.UserID))
|
||||
if err != nil {
|
||||
eh.log.Error().Err(err).Str("userID", req.UserID).Msg("could not compile regex")
|
||||
return err
|
||||
}
|
||||
|
||||
for _, i := range idx {
|
||||
e, err := eh.getEvent(i)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if userID.Match(e.Event) {
|
||||
resp.Events = append(resp.Events, e)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (eh *EventHistoryService) getEvent(id string) (*ehmsg.Event, error) {
|
||||
evs, err := eh.store.Read(id)
|
||||
if err != nil {
|
||||
if !errors.Is(err, store.ErrNotFound) {
|
||||
eh.log.Error().Err(err).Str("eventid", id).Msg("could not read event")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(evs) == 0 {
|
||||
return nil, store.ErrNotFound
|
||||
}
|
||||
|
||||
var ev StoreEvent
|
||||
if err := json.Unmarshal(evs[0].Value, &ev); err != nil {
|
||||
eh.log.Error().Err(err).Str("eventid", id).Msg("could not unmarshal event")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ehmsg.Event{
|
||||
Id: ev.ID,
|
||||
Event: ev.Event,
|
||||
Type: ev.Type,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestSearch(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Service Suite")
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/google/uuid"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
ehsvc "github.com/qsfera/server/protogen/gen/qsfera/services/eventhistory/v0"
|
||||
"github.com/qsfera/server/services/eventhistory/pkg/config"
|
||||
"github.com/qsfera/server/services/eventhistory/pkg/service"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/store"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
microevents "go-micro.dev/v4/events"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
var _ = Describe("EventHistoryService", func() {
|
||||
var (
|
||||
cfg = &config.Config{}
|
||||
|
||||
eh *service.EventHistoryService
|
||||
bus testBus
|
||||
sto microstore.Store
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
sto = store.Create()
|
||||
bus = testBus(make(chan events.Event))
|
||||
eh, err = service.NewEventHistoryService(cfg, bus, sto, log.Logger{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
close(bus)
|
||||
})
|
||||
|
||||
It("Records events, stores them and allows them to be retrieved", func() {
|
||||
id := bus.Publish(events.UploadReady{})
|
||||
|
||||
// service will store eventually
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
resp := &ehsvc.GetEventsResponse{}
|
||||
err := eh.GetEvents(context.Background(), &ehsvc.GetEventsRequest{Ids: []string{id}}, resp)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp).ToNot(BeNil())
|
||||
|
||||
Expect(len(resp.Events)).To(Equal(1))
|
||||
Expect(resp.Events[0].Id).To(Equal(id))
|
||||
})
|
||||
|
||||
It("Gets all events", func() {
|
||||
ids := make([]string, 3)
|
||||
ids[0] = bus.Publish(events.UploadReady{
|
||||
ExecutingUser: &userv1beta1.User{
|
||||
Id: &userv1beta1.UserId{
|
||||
OpaqueId: "test-id",
|
||||
},
|
||||
},
|
||||
Failed: false,
|
||||
Timestamp: utils.TimeToTS(time.Time{}),
|
||||
})
|
||||
ids[1] = bus.Publish(events.UserCreated{
|
||||
UserID: "another-id",
|
||||
})
|
||||
ids[2] = bus.Publish(events.UserDeleted{
|
||||
Executant: &userv1beta1.UserId{
|
||||
OpaqueId: "another-id",
|
||||
},
|
||||
UserID: "test-id",
|
||||
})
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
resp := &ehsvc.GetEventsResponse{}
|
||||
err := eh.GetEventsForUser(context.Background(), &ehsvc.GetEventsForUserRequest{UserID: "test-id"}, resp)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp).ToNot(BeNil())
|
||||
|
||||
// Events don't always come back in the same order as they were sent, so we need to sort them and
|
||||
// do the same for the expected IDs as well.
|
||||
expectedIDs := []string{ids[0], ids[2]}
|
||||
sort.Strings(expectedIDs)
|
||||
var gotIDs []string
|
||||
for _, ev := range resp.Events {
|
||||
gotIDs = append(gotIDs, ev.Id)
|
||||
}
|
||||
sort.Strings(gotIDs)
|
||||
|
||||
Expect(len(gotIDs)).To(Equal(len(expectedIDs)))
|
||||
Expect(gotIDs[0]).To(Equal(expectedIDs[0]))
|
||||
Expect(gotIDs[1]).To(Equal(expectedIDs[1]))
|
||||
})
|
||||
})
|
||||
|
||||
type testBus chan events.Event
|
||||
|
||||
func (tb testBus) Consume(_ string, _ ...microevents.ConsumeOption) (<-chan microevents.Event, error) {
|
||||
ch := make(chan microevents.Event)
|
||||
go func() {
|
||||
for ev := range tb {
|
||||
b, _ := json.Marshal(ev.Event)
|
||||
ch <- microevents.Event{
|
||||
Payload: b,
|
||||
Metadata: map[string]string{
|
||||
events.MetadatakeyEventID: ev.ID,
|
||||
events.MetadatakeyEventType: ev.Type,
|
||||
},
|
||||
}
|
||||
}
|
||||
}()
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (tb testBus) Publish(e any) string {
|
||||
ev := events.Event{
|
||||
ID: uuid.New().String(),
|
||||
Type: reflect.TypeOf(e).String(),
|
||||
Event: e,
|
||||
}
|
||||
|
||||
tb <- ev
|
||||
return ev.ID
|
||||
}
|
||||
Reference in New Issue
Block a user