summaryrefslogtreecommitdiffstats
path: root/lib/puppet/util/cacher.rb
blob: 3dddec0d42e8adc006102680f1a208ff27c4aed2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
require 'monitor'

module Puppet::Util::Cacher
  module Expirer
    attr_reader :timestamp

    # Cause all cached values to be considered expired.
    def expire
      @timestamp = Time.now
    end

    # Is the provided timestamp earlier than our expiration timestamp?
    # If it is, then the associated value is expired.
    def dependent_data_expired?(ts)
      return false unless timestamp

      timestamp > ts
    end
  end

  extend Expirer

  # Our module has been extended in a class; we can only add the Instance methods,
  # which become *class* methods in the class.
  def self.extended(other)
    class << other
      extend ClassMethods
      include InstanceMethods
    end
  end

  # Our module has been included in a class, which means the class gets the class methods
  # and all of its instances get the instance methods.
  def self.included(other)
    other.extend(ClassMethods)
    other.send(:include, InstanceMethods)
  end

  # Methods that can get added to a class.
  module ClassMethods
    # Provide a means of defining an attribute whose value will be cached.
    # Must provide a block capable of defining the value if it's flushed..
    def cached_attr(name, options = {}, &block)
      init_method = "init_#{name}"
      define_method(init_method, &block)

      define_method(name) do
        cached_value(name)
      end

      define_method(name.to_s + "=") do |value|
        # Make sure the cache timestamp is set
        cache_timestamp
        value_cache.synchronize { value_cache[name] = value }
      end

      if ttl = options[:ttl]
        set_attr_ttl(name, ttl)
      end
    end

    def attr_ttl(name)
      return nil unless @attr_ttls
      @attr_ttls[name]
    end

    def set_attr_ttl(name, value)
      @attr_ttls ||= {}
      @attr_ttls[name] = Integer(value)
    end
  end

  # Methods that get added to instances.
  module InstanceMethods

    def expire
      # Only expire if we have an expirer.  This is
      # mostly so that we can comfortably handle cases
      # like Puppet::Type instances, which use their
      # catalog as their expirer, and they often don't
      # have a catalog.
      if e = expirer
        e.expire
      end
    end

    def expirer
      Puppet::Util::Cacher
    end

    private

    def cache_timestamp
      @cache_timestamp ||= Time.now
    end

    def cached_value(name)
      value_cache.synchronize do
        # Allow a nil expirer, in which case we regenerate the value every time.
        if expired_by_expirer?(name)
          value_cache.clear
          @cache_timestamp = Time.now
        elsif expired_by_ttl?(name)
          value_cache.delete(name)
        end
        value_cache[name] = send("init_#{name}") unless value_cache.include?(name)
        value_cache[name]
      end
    end

    def expired_by_expirer?(name)
      if expirer.nil?
        return true unless self.class.attr_ttl(name)
      end
      expirer.dependent_data_expired?(cache_timestamp)
    end

    def expired_by_ttl?(name)
      return false unless self.class.respond_to?(:attr_ttl)
      return false unless ttl = self.class.attr_ttl(name)

      @ttl_timestamps ||= {}
      @ttl_timestamps[name] ||= Time.now

      (Time.now - @ttl_timestamps[name]) > ttl
    end

    def value_cache
      @value_cache ||= {}.extend(MonitorMixin)
    end
  end
end