From a55ea42d7f5900bd5fc8fad047040c7865824f33 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 15 Jun 2019 01:11:51 +0300 Subject: Unbreak things --- matrix/rooms/roomcache.go | 305 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 matrix/rooms/roomcache.go (limited to 'matrix/rooms/roomcache.go') diff --git a/matrix/rooms/roomcache.go b/matrix/rooms/roomcache.go new file mode 100644 index 0000000..03e3ad8 --- /dev/null +++ b/matrix/rooms/roomcache.go @@ -0,0 +1,305 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package rooms + +import ( + "compress/gzip" + "encoding/gob" + "os" + "path/filepath" + "time" + + "github.com/pkg/errors" + sync "github.com/sasha-s/go-deadlock" + + "maunium.net/go/gomuks/debug" +) + +// RoomCache contains room state info in a hashmap and linked list. +type RoomCache struct { + sync.Mutex + + listPath string + directory string + maxSize int + maxAge int64 + getOwner func() string + + Map map[string]*Room + head *Room + tail *Room + size int +} + +func NewRoomCache(listPath, directory string, maxSize int, maxAge int64, getOwner func() string) *RoomCache { + return &RoomCache{ + listPath: listPath, + directory: directory, + maxSize: maxSize, + maxAge: maxAge, + getOwner: getOwner, + + Map: make(map[string]*Room), + } +} + +func (cache *RoomCache) LoadList() error { + cache.Lock() + defer cache.Unlock() + + // Open room list file + file, err := os.OpenFile(cache.listPath, os.O_RDONLY, 0600) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return errors.Wrap(err, "failed to open room list file for reading") + } + defer debugPrintError(file.Close, "Failed to close room list file after reading") + + // Open gzip reader for room list file + cmpReader, err := gzip.NewReader(file) + if err != nil { + return errors.Wrap(err, "failed to read gzip room list") + } + defer debugPrintError(cmpReader.Close, "Failed to close room list gzip reader") + + // Open gob decoder for gzip reader + dec := gob.NewDecoder(cmpReader) + // Read number of items in list + var size int + err = dec.Decode(&size) + if err != nil { + return errors.Wrap(err, "failed to read size of room list") + } + + // Read list + cache.Map = make(map[string]*Room, size) + for i := 0; i < size; i++ { + room := &Room{} + err = dec.Decode(room) + if err != nil { + debug.Printf("Failed to decode %dth room list entry: %v", i+1, err) + continue + } + room.path = cache.roomPath(room.ID) + room.cache = cache + cache.Map[room.ID] = room + } + return nil +} + +func (cache *RoomCache) SaveLoadedRooms() { + cache.Lock() + defer cache.Unlock() + cache.clean() + for node := cache.head; node != nil; node = node.prev { + node.Save() + } +} + +func (cache *RoomCache) SaveList() error { + cache.Lock() + defer cache.Unlock() + + debug.Print("Saving room list...") + // Open room list file + file, err := os.OpenFile(cache.listPath, os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return errors.Wrap(err, "failed to open room list file for writing") + } + defer debugPrintError(file.Close, "Failed to close room list file after writing") + + // Open gzip writer for room list file + cmpWriter := gzip.NewWriter(file) + defer debugPrintError(cmpWriter.Close, "Failed to close room list gzip writer") + + // Open gob encoder for gzip writer + enc := gob.NewEncoder(cmpWriter) + // Write number of items in list + err = enc.Encode(len(cache.Map)) + if err != nil { + return errors.Wrap(err, "failed to write size of room list") + } + + // Write list + for _, node := range cache.Map { + err = enc.Encode(node) + if err != nil { + debug.Printf("Failed to encode room list entry of %s: %v", node.ID, err) + } + } + debug.Print("Room list saved to", cache.listPath, len(cache.Map), cache.size) + return nil +} + +func (cache *RoomCache) Touch(roomID string) { + cache.Lock() + node, ok := cache.Map[roomID] + if !ok || node == nil { + cache.Unlock() + return + } + cache.touch(node) + cache.Unlock() +} + +func (cache *RoomCache) TouchNode(node *Room) { + cache.Lock() + cache.touch(node) + cache.Unlock() +} + +func (cache *RoomCache) touch(node *Room) { + if node == cache.head { + return + } + debug.Print("Touching", node.ID) + cache.llPop(node) + cache.llPush(node) + node.touch = time.Now().Unix() +} + +func (cache *RoomCache) Get(roomID string) *Room { + cache.Lock() + node := cache.get(roomID) + cache.Unlock() + return node +} + +func (cache *RoomCache) GetOrCreate(roomID string) *Room { + cache.Lock() + node := cache.get(roomID) + if node == nil { + node = cache.newRoom(roomID) + cache.llPush(node) + } + cache.Unlock() + return node +} + +func (cache *RoomCache) get(roomID string) *Room { + node, ok := cache.Map[roomID] + if ok && node != nil && node.Loaded() { + cache.touch(node) + return node + } + return nil +} +func (cache *RoomCache) Put(room *Room) { + cache.Lock() + node := cache.get(room.ID) + if node != nil { + cache.touch(node) + } else { + cache.Map[room.ID] = room + if room.Loaded() { + cache.llPush(room) + } + node = room + } + cache.Unlock() + node.Save() +} + +func (cache *RoomCache) roomPath(roomID string) string { + return filepath.Join(cache.directory, roomID+".gob.gz") +} + +func (cache *RoomCache) Load(roomID string) *Room { + cache.Lock() + defer cache.Unlock() + node, ok := cache.Map[roomID] + if ok { + return node + } + + node = NewRoom(roomID, cache) + node.Load() + return node +} + +func (cache *RoomCache) llPop(node *Room) { + if node.prev == nil && node.next == nil { + return + } + if node.prev != nil { + node.prev.next = node.next + } + if node.next != nil { + node.next.prev = node.prev + } + if node == cache.tail { + cache.tail = node.next + } + if node == cache.head { + cache.head = node.prev + } + node.next = nil + node.prev = nil + cache.size-- +} + +func (cache *RoomCache) llPush(node *Room) { + if node.next != nil || node.prev != nil { + debug.PrintStack() + debug.Print("Tried to llPush node that is already in stack") + return + } + if node == cache.head { + return + } + if cache.head != nil { + cache.head.next = node + } + node.prev = cache.head + node.next = nil + cache.head = node + if cache.tail == nil { + cache.tail = node + } + cache.size++ + cache.clean() +} + +func (cache *RoomCache) clean() { + origSize := cache.size + maxTS := time.Now().Unix() - cache.maxAge + for cache.size > cache.maxSize { + if cache.tail.touch > maxTS { + break + } + cache.tail.Unload() + cache.llPop(cache.tail) + } + if cleaned := origSize - cache.size; cleaned > 0 { + debug.Print("Cleaned", cleaned, "rooms") + } +} + +func (cache *RoomCache) Unload(node *Room) { + cache.Lock() + defer cache.Unlock() + cache.llPop(node) + node.Unload() +} + +func (cache *RoomCache) newRoom(roomID string) *Room { + node := NewRoom(roomID, cache) + cache.Map[node.ID] = node + return node +} -- cgit v1.2.3