From 8861437c26f54e5ec1e38e8140e9aedd9ca9db1b Mon Sep 17 00:00:00 2001 From: Anant Prakash Date: Wed, 16 May 2018 16:06:40 +0530 Subject: [PATCH] Add module for transactions cache (#440) * Add transactions cache, write tests. Add a transactions module in dendrite/common. This is needed for idempotent APIs. Signed-off-by: Anant Prakash * Use cycling double map instead, improve code logic, remove unneeded test Signed-off-by: Anant Prakash * Update code comments Signed-off-by: Anant Prakash * Use two constructors for default and custom cleanupPeriod Add code comments Signed-off-by: Anant Prakash --- .../common/transactions/transactions.go | 88 +++++++++++++++++++ .../common/transactions/transactions_test.go | 50 +++++++++++ 2 files changed, 138 insertions(+) create mode 100644 src/github.com/matrix-org/dendrite/common/transactions/transactions.go create mode 100644 src/github.com/matrix-org/dendrite/common/transactions/transactions_test.go diff --git a/src/github.com/matrix-org/dendrite/common/transactions/transactions.go b/src/github.com/matrix-org/dendrite/common/transactions/transactions.go new file mode 100644 index 00000000..febcb9a7 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/common/transactions/transactions.go @@ -0,0 +1,88 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transactions + +import ( + "sync" + "time" + + "github.com/matrix-org/util" +) + +// DefaultCleanupPeriod represents the default time duration after which cacheCleanService runs. +const DefaultCleanupPeriod time.Duration = 30 * time.Minute + +type txnsMap map[string]*util.JSONResponse + +// Cache represents a temporary store for response entries. +// Entries are evicted after a certain period, defined by cleanupPeriod. +// This works by keeping two maps of entries, and cycling the maps after the cleanupPeriod. +type Cache struct { + sync.RWMutex + txnsMaps [2]txnsMap + cleanupPeriod time.Duration +} + +// New is a wrapper which calls NewWithCleanupPeriod with DefaultCleanupPeriod as argument. +func New() *Cache { + return NewWithCleanupPeriod(DefaultCleanupPeriod) +} + +// NewWithCleanupPeriod creates a new Cache object, starts cacheCleanService. +// Takes cleanupPeriod as argument. +// Returns a reference to newly created Cache. +func NewWithCleanupPeriod(cleanupPeriod time.Duration) *Cache { + t := Cache{txnsMaps: [2]txnsMap{make(txnsMap), make(txnsMap)}} + t.cleanupPeriod = cleanupPeriod + + // Start clean service as the Cache is created + go cacheCleanService(&t) + return &t +} + +// FetchTransaction looks up an entry for txnID in Cache. +// Looks in both the txnMaps. +// Returns (JSON response, true) if txnID is found, else the returned bool is false. +func (t *Cache) FetchTransaction(txnID string) (*util.JSONResponse, bool) { + t.RLock() + defer t.RUnlock() + for _, txns := range t.txnsMaps { + res, ok := txns[txnID] + if ok { + return res, true + } + } + return nil, false +} + +// AddTransaction adds an entry for txnID in Cache for later access. +// Adds to the front txnMap. +func (t *Cache) AddTransaction(txnID string, res *util.JSONResponse) { + t.Lock() + defer t.Unlock() + + t.txnsMaps[0][txnID] = res +} + +// cacheCleanService is responsible for cleaning up entries after cleanupPeriod. +// It guarantees that an entry will be present in cache for at least cleanupPeriod & at most 2 * cleanupPeriod. +// This cycles the txnMaps forward, i.e. back map is assigned the front and front is assigned an empty map. +func cacheCleanService(t *Cache) { + ticker := time.Tick(t.cleanupPeriod) + for range ticker { + t.Lock() + t.txnsMaps[1] = t.txnsMaps[0] + t.txnsMaps[0] = make(txnsMap) + t.Unlock() + } +} diff --git a/src/github.com/matrix-org/dendrite/common/transactions/transactions_test.go b/src/github.com/matrix-org/dendrite/common/transactions/transactions_test.go new file mode 100644 index 00000000..0cdb776c --- /dev/null +++ b/src/github.com/matrix-org/dendrite/common/transactions/transactions_test.go @@ -0,0 +1,50 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transactions + +import ( + "net/http" + "testing" + + "github.com/matrix-org/util" +) + +type fakeType struct { + ID string `json:"ID"` +} + +var ( + fakeTxnID = "aRandomTxnID" + fakeResponse = &util.JSONResponse{Code: http.StatusOK, JSON: fakeType{ID: "0"}} +) + +// TestCache creates a New Cache and tests AddTransaction & FetchTransaction +func TestCache(t *testing.T) { + fakeTxnCache := New() + fakeTxnCache.AddTransaction(fakeTxnID, fakeResponse) + + // Add entries for noise. + for i := 1; i <= 100; i++ { + fakeTxnCache.AddTransaction( + fakeTxnID+string(i), + &util.JSONResponse{Code: http.StatusOK, JSON: fakeType{ID: string(i)}}, + ) + } + + testResponse, ok := fakeTxnCache.FetchTransaction(fakeTxnID) + if !ok { + t.Error("Failed to retrieve entry for txnID: ", fakeTxnID) + } else if testResponse.JSON != fakeResponse.JSON { + t.Error("Fetched response incorrect. Expected: ", fakeResponse.JSON, " got: ", testResponse.JSON) + } +}