tmud-3.0.0/benchmark/
tmud-3.0.0/cmd/
tmud-3.0.0/cmd/objects/
tmud-3.0.0/cmd/tiny/
tmud-3.0.0/doc/SQLite/
tmud-3.0.0/doc/SQLite3/
tmud-3.0.0/doc/TernaryTrie/
tmud-3.0.0/farts/
tmud-3.0.0/lib/
tmud-3.0.0/lib/engine/
tmud-3.0.0/lib/farts/
tmud-3.0.0/logs/
#
# file::    cache.rb
# author::  Jon A. Lambert
# version:: 2.8.0
# date::    01/19/2006
#
# This source code copyright (C) 2005, 2006 by Jon A. Lambert
# All rights reserved.
#
# Released under the terms of the TeensyMUD Public License
# See LICENSE file for additional information.
#
$:.unshift "lib" if !$:.include? "lib"
$:.unshift "vendor" if !$:.include? "vendor"

require 'utility/configuration'
require 'utility/log'
require 'utility/utility'

# The object cache is limited in size since the purpose is to control the
# number of objects present in storage and provide a reasonably efficient
# means of looking them up. I don't use the Ruby hash implementation since
# we do want key collisions.  Its purpose is to avoid collisions.  Instead
# I implement hash table with single link list chaining using a Ruby array
# of arrays, a Ruby array being the closet equivalent to a link list.
#
# The first dimension is fixed as the cache_width and keys are hashed by
# key modulus cache_width. The second dimension, the cache_depth is the
# limit on how big the second array can grow.
#
# Everytime an object is not found in the cache, we pop an entry from
# then end of the cache list chain, save it if dirty, copy our new
# object into it, and place it at the head of the list chain.


# This class stores the cache statistics.
class CacheStats
  def initialize
    @stats = {}
  end

  def inc(k)
    @stats[k] ||= 0
    @stats[k] += 1
  end

  def each(&blk)
    @stats.each &blk
  end

end

# This is a node in the cache and is a wrapper for the object and keeps
# flags on the status of the entry.
class CacheEntry
  attr_accessor :oid, :obj

  # perhaps add LRU counters as CacheEntry is pretty dumb
  def initialize(oid=nil,obj=nil,dirty=false,noswap=false)
    @oid, @obj, @dirty, @noswap = oid, obj, dirty, noswap
  end

  # dirty cache entries are those that probably differ from the database
  def dirty?
    @dirty
  end

  def dirty!
    @dirty = true
  end

  def clean!
    @dirty = false
  end

  # A dead cache entry is an object that has been deleted
  def dead?
    @obj == nil
  end

  def mark_dead
    @obj = nil
    @oid = nil
    @dirty = false
    @noswap = false
  end

  # Noswappable cache entries are objects that have non persistent
  # attributes.  *heavy sigh*
  # We can never remove these from the cache until specifically marked
  # See makenoswap and makeswap
  def noswap?
    @noswap
  end

  def noswap!
    @noswap = true
  end

  def swap!
    @noswap = false
  end

end

# This class manages the cache.  It is initialized by the Store and front ends
# the database behind the Store.
class CacheManager
  configuration
  logger

  def initialize(db)
    @cwidth = options['cache_width']
    @cdepth = options['cache_depth']

    @db =  db
    @st = CacheStats.new

    @cache = Array.new(@cwidth) {Array.new}
    @cwidth.times do |i|
      @cdepth.times do |j|
        @cache[i] << CacheEntry.new
        @st.inc(:cache_size)
      end
    end

  end

  # report on the cache map  see cmd_dumpcache
  def inspect
    str = "cache width:#{@cwidth} cache depth:#{@cdepth}\n"
    @cwidth.times do |i|
      @cache[i].each_with_index do |ce,j|
        next if ce.dead?
        str << "cache map [#{i}][#{j}] => "
        str << "oid #{ce.oid} object_id #{ce.obj.object_id} dirty? #{ce.dirty?}  noswap? #{ce.noswap?}\n"
      end
    end
    str
  end

  # Our simple hash algoritm.  Our database keys are sequentially
  # assigned integers, so... I don't know what would be better.
  def hash(oid)
    oid % @cwidth
  end

  # gets an object
  # [+oid+] - integer object id to retrieve
  # [+return+] - a reference to the object or nil if none exists
  def get(oid)
    return nil if oid.nil?
    @st.inc(:reads)
    hv = hash(oid)

    # search the cache
    @cache[hv].each do |ce|
      if ce.oid == oid
        @st.inc(:cache_read_hits)
        # need to try putting hot hit to the head of the list?
        return ce.obj
      end
    end

    # cache miss - search the database
    if @db.has_key? oid.to_s
      ret = Utility.decode(@db[oid.to_s])
    else
      @st.inc(:database_read_fails)
      return nil
    end
    @st.inc(:database_reads)

    # get and remove the last entry off this list
    ch = @cache[hv].pop
    # if its dirty we write it to the database
    if ch.dirty? && !ch.dead?
      @db[ch.oid.to_s] = Utility.encode(ch.obj)
      @st.inc(:database_writes)
      if ch.noswap?  # here we have a problem we can't use this
        # first push it back onto the list
        @cache[hv].unshift ch
        # get ourselves a brand new
        ch = CacheEntry.new
        # problem solved
        # the depth of any list chains will be cache_depth + # noswap entries
      end
    end

    # assign our new object to the cache entry
    ch.obj = ret
    ch.oid = oid
    ch.clean!
    # push it to the head of the list
    @cache[hv].unshift ch

    ret
  end

  # puts an object
  # [+obj+] - The integer object id to retrieve
  # [+return+] - undefined
  def put(obj)
    return nil if obj.nil?
    @st.inc(:writes)
    hv = hash(obj.id)

    # search the cache
    @cache[hv].each do |ce|
      next if ce.oid != obj.id
      @st.inc(:cache_write_hits) if ce.dirty?
      if obj.object_id != ce.obj.object_id  # be safe
        # Be extra careful here.  It's possible we could have two objects
        # with the same object id but with different Ruby object_ids.
        # Most likely this is a bug, but we should handle it here..
        ce.obj = obj
        log.warn "Duplicate object id's in cache!"
        log.warn "insert - #{obj.inspect}"
        log.warn "cache - #{obj.inspect}"
      end
      # need to try putting hot hit to the head of the list?
      ce.dirty!
      return
    end

    # get and remove the last entry off this list
    ch = @cache[hv].pop
    # if its dirty we write it to the database
    if ch.dirty? && !ch.dead?
      # errors possible - check in store module
      @db[ch.oid.to_s] = Utility.encode(ch.obj)
      @st.inc(:database_writes)
      if ch.noswap?  # here we have a problem we can't use this
        # first push it back onto the list
        @cache[hv].unshift ch
        # get ourselves a brand new
        ch = CacheEntry.new
        # problem solved
        # the depth of any list chains will be cache_depth + # noswap entries
      end
    end

    # assign our new object to the cache entry
    ch.obj = obj
    ch.oid = obj.id
    ch.dirty!
    # push it to the head of the list
    @cache[hv].unshift ch

    return
  end

  # delete an object
  def delete(oid)
    return nil if oid.nil?
    @st.inc(:deletes)
    hv = hash(oid)

    # mark dead in cache
    @cache[hv].each do |ce|
      next if ce.oid != oid
      ce.mark_dead
      break
    end
    # delete it from the database
    @db.delete(oid.to_s)
  end

  # deliberately mark an object in the cache as dirty
  # see properties.
  def mark(oid)
    return nil if oid.nil?
    @st.inc(:cache_marks)
    hv = hash(oid)

    @cache[hv].each do |ce|
      next if ce.oid != oid
      ce.dirty!
      return
    end

    # this would indicate we've tried marking it dirty before we did a put.
    @st.inc(:cache_mark_misses)
    log.debug "Marking object dirty before put - #{oid}"
  end

  # syncronize the entire cache with the database
  # called by save.
  def sync
    @st.inc(:cache_syncs)
    # search the list chains for dirty objects and write them out.
    @cwidth.times do |i|
      @cache[i].each do |ce|
        if ce.dirty?
          @db[ce.oid.to_s] = Utility.encode(ce.obj)
          @st.inc(:database_writes)
          ce.clean!
        end
      end
    end
  end

  # syncronize a specific list chain
  # not yet called but included for possible performance enhancement
  def sync_chain(i)
    @st.inc(:chain_syncs)
    @cache[i].each do |ce|
      if ce.dirty?
        @db[ce.oid.to_s] = Utility.encode(ce.obj)
        @st.inc(:database_writes)
        ce.clean!
      end
    end
  end

  # Mark an object in the cache as nonswappable
  # [+oid+] is the object id
  # [+return+] undefined
  def makenoswap(oid)
    return nil if oid.nil?
    @st.inc(:cache_noswap)
    hv = hash(oid)

    @cache[hv].each do |ce|
      next if ce.oid != oid
      ce.noswap!
      return
    end

    # This would indicate we've tried marking it noswap before we did a put.
    # A logic error
    @st.inc(:cache_noswap_misses)
    log.debug "Marking object nonswappable before put - #{oid}"
  end

  # Mark an object in the cache as swappable
  # [+oid+] is the object id
  # [+return+] undefined
  def makeswap(oid)
    return nil if oid.nil?
    @st.inc(:cache_swap)
    hv = hash(oid)

    @cache[hv].each do |ce|
      next if ce.oid != oid
      ce.swap!
      return
    end

    # This would indicate we've tried marking it swap before we did a put.
    # A logic error
    @st.inc(:cache_swap_misses)
    log.debug "Marking object swappable before put - #{oid}"
  end

  # This is provided to traverse the entire object chain.
  # This thrashes the cache violently.  See Store#each.
  # One needs to question the design of routines that use Store#each above.
  def keys
    kys = []
    @cwidth.times do |i|
      @cache[i].each {|ce| kys << ce.oid if !ce.oid.nil? }
    end
    kys
  end

  # produce a report - see cmd_stats
  def stats
    str =  "----------* Cache Statistics *----------\n"
    @st.each do |k,v|
      str << sprintf("%25-25s : %d\n",k.to_s.gsub(/_/,' '),v)
    end
    str << "----------*                  *----------\n"
  end

end