Skip to content
This repository has been archived by the owner on Oct 28, 2020. It is now read-only.

[wip] User cache support. #13

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

SuperSpyTX
Copy link
Collaborator

@SuperSpyTX SuperSpyTX commented Sep 12, 2017

Added preliminary support for user caching. When the module is loaded and enabled, it loads the cached database entries and inserts them directly into the RTM object client.users. In addition, any calls to client.ReplaceManyUserObjects and client.ReplaceUserObject will have the cache updated.

In addition to these changes, I added the delay from the Retry-After http header returned if the call was ratelimited.

Questions:

  • Should I take a different approach? Currently I modified the two functions above to call the user cache module if it's loaded.
  • Is adding the delay from the Retry-After header a good idea? It does fill the user cache (all 2,600 users) successfully.

Overall, do you think this is a good implementation?

Copy link
Owner

@riking riking left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks mostly good! Some perf problems, though.


// $1 = slack.UserID
// $2 = data (json encoded)
sqlAddEntry = `INSERT INTO module_user_cache (user_id,data) VALUES ($1, $2)`
Copy link
Owner

@riking riking Sep 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ON CONFLICT DO UPDATE SET data = EXCLUDED.data
😃

Avoids the need for a transaction / checking first.

if err != nil {
return errors.Wrap(err, "error in user cache: unmarshal user object")
}
rtmClient := mod.team.GetRTMClient().(*rtm.Client)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

save this outside the for loop

for stmt.Next() {
var id string
var data string
var user slack.User
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pointer to slack.User, remove & on line 74. The object's going to live on the heap anyways.

type userCacheAPI interface {
marvin.Module

GetEntry(userid slack.UserID) (slack.User, error)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetEntry is never used and can be dropped from the interface.

"github.com/riking/marvin/slack"
)

type API interface {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API type can be dropped, or at least commented with // interface duplicated in rtm package


import (
"sync"

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run goimports

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could have sworn goimports was run on this file and this was the result... I'll run it again and see if it produces the same result.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, remove the newline between the two import blocks.

return err
}

func (mod *UserCacheModule) UpdateEntries(userobjects []*slack.User) error {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider forwarding UpdateEntry to this function instead - you can prepare the statement once and do many inserts.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was also considering using transactions (.Begin and .Commit).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see what you mean.

return stmt.Err()
}

func (mod *UserCacheModule) UpdateEntry(userobject slack.User) error {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should take a pointer - slack.User is a large object.

return errors.Wrap(err, "error in user cache: unmarshal user object")
}
rtmClient := mod.team.GetRTMClient().(*rtm.Client)
rtmClient.ReplaceUserObject(&user)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method takes a lock - consider batching by hundreds?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to do an big array and loading it that way, but I didn't want to consume so much memory. Perhaps maybe I can make this so every 200 entries (like the Slack API) it'll call ReplaceManyUserObjects with an array of 200, then empty and continue.

for response.PageInfo.NextCursor != "" {
c.ReplaceManyUserObjects(response.Members)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to make this change because otherwise the ReplaceManyUserObjects would get called again for the same group of objects retrieved from the last successful query.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And with the new changes, it was also not retrieving all the users.

for stmt.Next() {
var id string
var data string
var user slack.User
var user *slack.User = &slack.User{}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leave it at nil, json.Unmarshal(&user) will allocate for you.

if err != nil {
return errors.Wrap(err, "error in user cache: unmarshal user object")
return err
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably safer to skip erroring rows - we'll re-fetch the data later.

arr = append(arr, user)
if len(arr) >= 199 {
go rtmClient.ReplaceManyUserObjects(arr, false)
arr = make([]*slack.User, 200)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function isn't blocking boot, so just call ReplaceMany directly and use arr = arr[:0] instead.

}

func (mod *UserCacheModule) Enable(team marvin.Team) {
go func() {
fmt.Printf("Loading cache entries....\n")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user cache, until we're caching other objects as well

func (mod *UserCacheModule) UpdateEntry(userobject *slack.User) error {
var objarray = make([]*slack.User, 1)
objarray[0] = userobject
return mod.UpdateEntries(objarray)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mod.UpdateEntries([]*slack.User{userobject})

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to figure out what the easier way was...ugh.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's type{fields}

if err != nil {
return err
entrydata, err := json.Marshal(obj)
if err == nil {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if err != nil { continue } on json marshalling

Copy link
Owner

@riking riking left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to change the callers of rtm.Client.fillUsersList() - it doesn't need to be called on boot because we have the cache.

moduleCacheApi := c.team.GetModule("usercache")
if moduleCacheApi != nil && updateCache {
cacheApi = moduleCacheApi.(userCacheAPI)
cacheApi.UpdateEntries(objs)
Copy link
Owner

@riking riking Sep 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this outside the MetadataLock.

moduleCacheApi := c.team.GetModule("usercache")
if moduleCacheApi != nil {
cacheApi = moduleCacheApi.(userCacheAPI)
cacheApi.UpdateEntry(obj)
Copy link
Owner

@riking riking Sep 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same - move outside the lock.

@SuperSpyTX
Copy link
Collaborator Author

If you would like, I can squash these commits (like the old bukkit days).

@SuperSpyTX
Copy link
Collaborator Author

And yes, I've been testing my code.

delaystr, _, _ := mod.team.ModuleConfig(Identifier).GetIsDefault("delay")
timeint, _ := strconv.ParseInt(timestr, 10, 64)
var timeres = time.Unix(timeint, 0)
delayres, err := time.ParseDuration(delaystr)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should fall back to default if it fails - const defaultDelay = 72*time.Hour; if err != nil { delayres = defaultDelay }

Fail safe, not dangerous - setting a bad duration would result in reloading the entire thing every hour.

@SuperSpyTX
Copy link
Collaborator Author

Is this still an problem a year later?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants