Permalink
Browse files

fs: Infer actor from path and provided users.User.

As a result, it can be omitted from stored eventDisk JSON object.

This helps reduce storage requirements, duplication of information,
and ensures that the latest actor user data is used for all events,
including older ones.
  • Loading branch information...
dmitshur committed Nov 30, 2017
1 parent 6e7b11c commit 3979195154f373f8144f4ec6d0b9e2f541f7a7e0
Showing with 27 additions and 115 deletions.
  1. +9 −9 fs/fs.go
  2. +1 −1 fs/fs_test.go
  3. +16 −104 fs/schema.go
  4. +1 −1 githubapi/githubapi.go
View
@@ -15,8 +15,8 @@ import (
)
// NewService creates a virtual filesystem-backed events.Service,
// using root for storage.
func NewService(root webdav.FileSystem, user users.UserSpec) (events.Service, error) {
// using root for storage. It logs and fetches events only for the specified user.
func NewService(root webdav.FileSystem, user users.User) (events.Service, error) {
s := &service{
fs: root,
user: user,
@@ -30,26 +30,26 @@ func NewService(root webdav.FileSystem, user users.UserSpec) (events.Service, er
type service struct {
fs webdav.FileSystem
user users.UserSpec
user users.User
mu sync.Mutex
ring ring
events [ringSize]event.Event // Latest events are added to the end.
}
func (s *service) load() error {
err := jsonDecodeFile(context.Background(), s.fs, ringPath(s.user), &s.ring)
err := jsonDecodeFile(context.Background(), s.fs, ringPath(s.user.UserSpec), &s.ring)
if err != nil && !os.IsNotExist(err) {
return err
}
for i := 0; i < s.ring.Length; i++ {
idx := s.ring.At(i)
var event eventDisk
err := jsonDecodeFile(context.Background(), s.fs, eventPath(s.user, idx), &event)
err := jsonDecodeFile(context.Background(), s.fs, eventPath(s.user.UserSpec, idx), &event)
if err != nil {
return err
}
s.events[idx] = event.Event()
s.events[idx] = event.Event(s.user)
}
return nil
}
@@ -72,7 +72,7 @@ func (s *service) Log(ctx context.Context, event event.Event) error {
return errors.New("event.Time time zone must be UTC")
}
if event.Actor.UserSpec != s.user {
if event.Actor.UserSpec != s.user.UserSpec {
// Skip other users.
return nil
}
@@ -84,11 +84,11 @@ func (s *service) Log(ctx context.Context, event event.Event) error {
// Commit to storage first, returning error on failure.
// Write the event file, then write the ring file, so that partial failure is less bad.
err := jsonEncodeFile(ctx, s.fs, eventPath(s.user, idx), fromEvent(event))
err := jsonEncodeFile(ctx, s.fs, eventPath(s.user.UserSpec, idx), fromEvent(event))
if err != nil {
return err
}
err = jsonEncodeFile(ctx, s.fs, ringPath(s.user), ring)
err = jsonEncodeFile(ctx, s.fs, ringPath(s.user.UserSpec), ring)
if err != nil {
return err
}
View
@@ -18,7 +18,7 @@ func Test(t *testing.T) {
if err != nil {
t.Fatal(err)
}
s, err := fs.NewService(mem, mockUser.UserSpec)
s, err := fs.NewService(mem, mockUser)
if err != nil {
t.Fatal(err)
}
View
@@ -65,23 +65,21 @@ func (r ring) Next() (ring ring, idx int) {
}
// eventDisk is an on-disk representation of event.Event.
// Actor is omitted from struct because it's encoded as part of event file path.
type eventDisk struct {
Time time.Time
Actor users.User
Container string
Payload interface{}
}
func (e eventDisk) MarshalJSON() ([]byte, error) {
v := struct {
Time time.Time
Actor user
Container string
Type string
Payload interface{}
}{
Time: e.Time,
Actor: fromUser(e.Actor),
Container: e.Container,
}
switch p := e.Payload.(type) {
@@ -129,7 +127,6 @@ func (e *eventDisk) UnmarshalJSON(b []byte) error {
}
var v struct {
Time time.Time
Actor user
Container string
Type string
Payload json.RawMessage
@@ -140,7 +137,6 @@ func (e *eventDisk) UnmarshalJSON(b []byte) error {
}
*e = eventDisk{}
e.Time = v.Time
e.Actor = v.Actor.User()
e.Container = v.Container
switch v.Type {
case "issue":
@@ -225,11 +221,23 @@ func (e *eventDisk) UnmarshalJSON(b []byte) error {
}
func fromEvent(e event.Event) eventDisk {
return eventDisk(e)
return eventDisk{
Time: e.Time,
// Omit Actor because it's encoded as part of event file path.
Container: e.Container,
Payload: e.Payload,
}
}
func (e eventDisk) Event() event.Event {
return event.Event(e)
// Event converts eventDisk to event.Event, using actor
// inferred from event file path.
func (e eventDisk) Event(actor users.User) event.Event {
return event.Event{
Time: e.Time,
Actor: actor,
Container: e.Container,
Payload: e.Payload,
}
}
// issue is an on-disk representation of event.Issue.
@@ -463,99 +471,3 @@ func fromPage(p event.Page) page {
func (p page) Page() event.Page {
return event.Page(p)
}
// user is an on-disk representation of users.User.
// TODO: Consider storing user spec only, fetching user from users.Service,
// or better yet, using provided users.User?
type user struct {
ID uint64
Domain string `json:",omitempty"`
Elsewhere []userSpec `json:",omitempty"`
Login string
Name string `json:",omitempty"`
Email string `json:",omitempty"`
AvatarURL string `json:",omitempty"`
HTMLURL string `json:",omitempty"`
CreatedAt *time.Time `json:",omitempty"`
UpdatedAt *time.Time `json:",omitempty"`
SiteAdmin bool `json:",omitempty"`
}
func fromUser(u users.User) user {
var elsewhere []userSpec
for _, us := range u.Elsewhere {
elsewhere = append(elsewhere, fromUserSpec(us))
}
return user{
ID: u.ID,
Domain: u.Domain,
Elsewhere: elsewhere,
Login: u.Login,
Name: u.Name,
Email: u.Email,
AvatarURL: u.AvatarURL,
HTMLURL: u.HTMLURL,
CreatedAt: timePointer(u.CreatedAt),
UpdatedAt: timePointer(u.UpdatedAt),
SiteAdmin: u.SiteAdmin,
}
}
func (u user) User() users.User {
var elsewhere []users.UserSpec
for _, us := range u.Elsewhere {
elsewhere = append(elsewhere, us.UserSpec())
}
return users.User{
UserSpec: users.UserSpec{
ID: u.ID,
Domain: u.Domain,
},
Elsewhere: elsewhere,
Login: u.Login,
Name: u.Name,
Email: u.Email,
AvatarURL: u.AvatarURL,
HTMLURL: u.HTMLURL,
CreatedAt: timeValue(u.CreatedAt),
UpdatedAt: timeValue(u.UpdatedAt),
SiteAdmin: u.SiteAdmin,
}
}
func timePointer(t time.Time) *time.Time {
if t.IsZero() {
return nil
}
return &t
}
func timeValue(t *time.Time) time.Time {
if t == nil {
return time.Time{}
}
return *t
}
// userSpec is an on-disk representation of users.UserSpec.
type userSpec struct {
ID uint64
Domain string `json:",omitempty"`
}
func fromUserSpec(us users.UserSpec) userSpec {
return userSpec(us)
}
func (us userSpec) UserSpec() users.UserSpec {
return users.UserSpec(us)
}
View
@@ -20,7 +20,7 @@ import (
)
// NewService creates a GitHub-backed events.Service using given GitHub client.
// It fetches the events for the specified user. user.Domain must be "github.com".
// It fetches events only for the specified user. user.Domain must be "github.com".
func NewService(client *github.Client, user users.User) (events.Service, error) {
if user.Domain != "github.com" {
return nil, fmt.Errorf(`user.Domain is %q, it must be "github.com"`, user.Domain)

0 comments on commit 3979195

Please sign in to comment.