view pkg/octree/cache.go @ 2465:86c7a023400e octree-diff

Started experimental octree diff branch.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Mon, 25 Feb 2019 17:02:33 +0100
parents f4dcbe8941a1
children 49564382ffff
line wrap: on
line source

// This is Free Software under GNU Affero General Public License v >= 3.0
// without warranty, see README.md and license for details.
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// License-Filename: LICENSES/AGPL-3.0.txt
//
// Copyright (C) 2018 by via donau
//   – Österreichische Wasserstraßen-Gesellschaft mbH
// Software engineering by Intevation GmbH
//
// Author(s):
//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>

package octree

import (
	"context"
	"database/sql"
	"sync"
	"time"
)

type (
	cacheKey struct {
		date       time.Time
		bottleneck string
	}

	cacheEntry struct {
		checksum string
		tree     *Tree
		access   time.Time
	}

	// Cache holds Octrees for a defined amount of time in memory
	// before they are released.
	Cache struct {
		sync.Mutex
		entries map[cacheKey]*cacheEntry
	}
)

const (
	cleanupCacheSleep = 6 * time.Minute
	maxCacheAge       = 5 * time.Minute
	maxCacheEntries   = 4
)

const (
	fetchOctreeSQL = `
SELECT octree_checksum, octree_index
FROM waterway.sounding_results
WHERE bottleneck_id = $1 AND date_info = $2::date
  AND octree_checksum IS NOT NULL AND octree_index IS NOT NULL
`
	checkOctreeSQL = `
SELECT CASE
  WHEN octree_checksum = $3 THEN NULL
  ELSE octree_index
  END
FROM waterway.sounding_results
WHERE bottleneck_id = $1 AND date_info = $2::date
  AND octree_checksum IS NOT NULL AND octree_index IS NOT NULL
`
)

var cache = Cache{
	entries: map[cacheKey]*cacheEntry{},
}

func init() {
	go cache.background()
}

func (c *Cache) background() {
	for {
		time.Sleep(cleanupCacheSleep)
		c.cleanup()
	}
}

func (c *Cache) cleanup() {
	c.Lock()
	defer c.Unlock()
	good := time.Now().Add(-maxCacheAge)
	for k, v := range c.entries {
		if v.access.Before(good) {
			delete(c.entries, k)
		}
	}
}

// FromCache fetches an Octree from the global Octree cache.
func FromCache(
	ctx context.Context,
	conn *sql.Conn,
	bottleneck string, date time.Time,
) (*Tree, error) {
	return cache.get(ctx, conn, bottleneck, date)
}

func (c *Cache) get(
	ctx context.Context,
	conn *sql.Conn,
	bottleneck string, date time.Time,
) (*Tree, error) {
	c.Lock()
	defer c.Unlock()

	key := cacheKey{date, bottleneck}
	entry := c.entries[key]

	var data []byte
	var checksum string

	if entry == nil {
		// fetch from database
		err := conn.QueryRowContext(
			ctx, fetchOctreeSQL, bottleneck, date).Scan(&checksum, &data)
		switch {
		case err == sql.ErrNoRows:
			return nil, nil
		case err != nil:
			return nil, err
		}
	} else {
		// check if we are not outdated.
		err := conn.QueryRowContext(
			ctx, checkOctreeSQL, bottleneck, date, entry.checksum).Scan(&data)
		switch {
		case err == sql.ErrNoRows:
			return nil, nil
		case err != nil:
			return nil, err
		}
		if data == nil { // we are still current
			entry.access = time.Now()
			return entry.tree, nil
		}
	}

	tree, err := Deserialize(data)
	if err != nil {
		return nil, err
	}

	now := time.Now()

	if entry != nil {
		entry.tree = tree
		entry.access = now
		return tree, nil
	}

	for len(c.entries) >= maxCacheEntries {
		// Evict the entry that is accessed the longest time ago.
		var oldestKey cacheKey
		oldest := now

		for k, v := range c.entries {
			if v.access.Before(oldest) {
				oldest = v.access
				oldestKey = k
			}
		}
		delete(c.entries, oldestKey)
	}

	c.entries[key] = &cacheEntry{
		checksum: checksum,
		tree:     tree,
		access:   now,
	}

	return tree, nil
}