Skip to content
This repository has been archived by the owner on Nov 25, 2024. It is now read-only.

Add module for transactions cache #440

Merged
merged 4 commits into from
May 16, 2018

Conversation

APwhitehat
Copy link
Contributor

@APwhitehat APwhitehat commented May 10, 2018

Add a transactions module in dendrite/common. This is needed for idempotent APIs.

based on #419.

Add a transactions module in dendrite/common. This is needed for idempotent APIs.

Signed-off-by: Anant Prakash <anantprakashjsr@gmail.com>
@erikjohnston erikjohnston self-assigned this May 13, 2018
Copy link
Member

@erikjohnston erikjohnston left a comment

Choose a reason for hiding this comment

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

This looks good! There's just a couple of comments.


// FetchTransaction looks up an entry for txnID in Cache.
// Returns a JSON response if txnID is found, else returns error.
func (t *Cache) FetchTransaction(txnID string) (*util.JSONResponse, error) {
Copy link
Member

Choose a reason for hiding this comment

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

I suspect the API will be easier to use if we return a bool instead of an error

func cacheCleanService(t *Cache) {
for {
time.Sleep(CleanupPeriod)
go clean(t)
Copy link
Member

Choose a reason for hiding this comment

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

I don't think there is much reason to spawn a goroutine here, it doesn't matter if we're imprecise about the timings so its fine to block the loop while we do the clean up.

(FWIW, if we did care about getting a steady rate then we shouldn't use a loop with time.Sleep, as its very easy for the timings to start to drift. In go, you'd probably use time.Ticker)


func clean(t *Cache) {
expire := time.Now().Add(-CleanupPeriod)
for key := range t.txns {
Copy link
Member

Choose a reason for hiding this comment

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

This loop over t.txns needs to be in the lock too.

func TestConcurentAccess(t *testing.T) {
fakeTxnCache := New()
// Signal that this test should run in parallel
t.Parallel()
Copy link
Member

Choose a reason for hiding this comment

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

This only tells the test runner it can run this test in parallel with other tests (I believe), so this doesn't do what you want.

for i := 1; i <= 1000; i++ {
go check(t, fakeTxnCache, i)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Honestly, I'm not sure this test is really testing anything much; if there is a concurrency bug then this is unlikely to pick it up. The concurrency model here is to simply use RWLock, so I think its safe to simply not have a unit test and to manually make sure we lock in the appropriate methods.

(If the cache was more complicated, then we'd probably want to add hooks into the cache to let us write deterministic tests.)

)

// CleanupPeriod represents time in nanoseconds after which cacheCleanService runs.
const CleanupPeriod time.Duration = 30 * time.Minute
Copy link
Member

Choose a reason for hiding this comment

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

Personally I would make this an option for Cache

@erikjohnston
Copy link
Member

Thinking about it, I wonder if we should be a little concerned about the clean up method. We'll need to lock for during iteration of the map, which could take a little while if the map is large.

It's probably fine for now, but an alternative trick that you can use to make the clean up efficient is to:

  1. Have two maps instead of one
  2. During lookup check both maps
  3. Add new transactions to the first map
  4. Every N minutes: set the second map point to the first, and set the first map to a new empty map.

This works by effectively rotating the maps. A transaction will get added to the first map, it will at some point get rotated to the second map, and finally (after at least N minutes) the map with the transaction gets dropped.

Signed-off-by: Anant Prakash <anantprakashjsr@gmail.com>
Signed-off-by: Anant Prakash <anantprakashjsr@gmail.com>
Copy link
Member

@erikjohnston erikjohnston left a comment

Choose a reason for hiding this comment

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

Looks good! Just needs a quick comment about how this works

// New creates a new Cache object, starts cacheCleanService.
// Takes cleanupPeriod (in minutes) as argument, in case of 0 DefaultCleanupPeriod is used instead.
// Returns a reference to newly created Cache.
func New(cleanupPeriodInMins int) *Cache {
Copy link
Member

Choose a reason for hiding this comment

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

Given we have a time.Duration type, I'd use that. If you want to make it optional then either take a pointer and check for null or have New() and NewWithPeriod(..)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In most scenario one would let the Default be as it is, passing nil for that case is cumbersome.
So I will avoid the nil-able pointer parameter.

// Entries are evicted after a certain period, defined by cleanupPeriod.
type Cache struct {
sync.RWMutex
txnsMaps [2]txnsMap
Copy link
Member

Choose a reason for hiding this comment

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

I'd probably give a quick comment here to say why there are two maps, and how this all works internally

Add code comments

Signed-off-by: Anant Prakash <anantprakashjsr@gmail.com>
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