Class: ScoutApm::Layaway

Inherits:
Object
  • Object
show all
Defined in:
lib/scout_apm/layaway.rb

Constant Summary collapse

STALE_AGE =

How long to let a stale file sit before deleting it. Letting it sit a bit may be useful for debugging

10 * 60
MAX_FILES_LIMIT =

Failsafe to prevent writing layaway files if for some reason they are not being cleaned up

5000
TIME_FORMAT =

A strftime format string for how we render timestamps in filenames. Must be sortable as an integer

"%Y%m%d%H%M"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(context) ⇒ Layaway

Returns a new instance of Layaway.



22
23
24
# File 'lib/scout_apm/layaway.rb', line 22

def initialize(context)
  @context = context
end

Instance Attribute Details

#contextObject (readonly)

Returns the value of attribute context.



21
22
23
# File 'lib/scout_apm/layaway.rb', line 21

def context
  @context
end

Instance Method Details

#delete_files_for(timestamp) ⇒ Object



131
132
133
134
135
136
# File 'lib/scout_apm/layaway.rb', line 131

def delete_files_for(timestamp)
  all_files_for(timestamp).each { |layaway|
    logger.debug("Layaway: Deleting file: #{layaway}")
    File.unlink(layaway)
  }
end

#delete_stale_files(older_than) ⇒ Object



138
139
140
141
142
143
144
145
146
147
148
# File 'lib/scout_apm/layaway.rb', line 138

def delete_stale_files(older_than)
  all_files_for(:all).
    map { |filename| timestamp_from_filename(filename) }.
    compact.
    uniq.
    select { |timestamp| timestamp.to_i < older_than.strftime(TIME_FORMAT).to_i }.
      tap  { |timestamps| logger.debug("Layaway: Deleting stale files with timestamps: #{timestamps.inspect}") }.
    map    { |timestamp| delete_files_for(timestamp) }
rescue => e
  logger.debug("Layaway: Problem deleting stale files: #{e.message}, #{e.backtrace.inspect}")
end

#directoryObject

Returns a Pathname object with the fully qualified directory where the layaway files can be placed. That directory must be writable by this process.

Don’t set this in initializer, since it relies on agent instance existing to figure out the value.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/scout_apm/layaway.rb', line 35

def directory
  return @directory if @directory

  data_file = context.config.value("data_file")
  data_file = File.dirname(data_file) if data_file && !File.directory?(data_file)

  candidates = [
    data_file,
    "#{context.environment.root}/tmp",
    "/tmp"
  ].compact

  found = candidates.detect { |dir| File.writable?(dir) }
  logger.debug("Storing Layaway Files in #{found}")
  @directory = Pathname.new(found)
end

#loggerObject



26
27
28
# File 'lib/scout_apm/layaway.rb', line 26

def logger
  context.logger
end

#with_claim(timestamp) ⇒ Object

Claims a given timestamp by getting an exclusive lock on a timestamped coordinator file. The coordinator file never contains data, it’s just a syncronization mechanism.

Once the ‘claim’ is obtained:

* load and yield each ReportingPeriod from the layaway files.
* if there are reporting periods:
  * yields any ReportingPeriods collected up from all the files.
  * deletes all of the layaway files (including the coordinator) for the timestamp
* if not
  * delete the coordinator
* remove any stale layaway files that may be hanging around.
* Finally unlock and ensure the coordinator file is cleared.

If a claim file can’t be obtained, return false without doing any work Another process is handling the reporting.



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
# File 'lib/scout_apm/layaway.rb', line 83

def with_claim(timestamp)
  coordinator_file = glob_pattern(timestamp, :coordinator)

  begin
    # This file gets deleted only by a process that successfully created and obtained the exclusive lock
    f = File.open(coordinator_file, File::RDWR | File::CREAT | File::EXCL | File::NONBLOCK)
  rescue Errno::EEXIST
    false
  end

  begin
    if f
      begin
        logger.debug("Obtained Reporting Lock")

        log_layaway_file_information

        files = all_files_for(timestamp).reject{|l| l.to_s == coordinator_file.to_s }
        rps = files.map{ |layaway| LayawayFile.new(context, layaway).load }.compact
        if rps.any?
          yield rps

          logger.debug("Layaway: Deleting the now-reported files for #{timestamp.to_s}")
          delete_files_for(timestamp) # also removes the coodinator_file
        else
          File.unlink(coordinator_file)
          logger.debug("Layaway: No files to report")
        end

        logger.debug("Layaway: Checking for any stale files")
        delete_stale_files(timestamp.to_time - STALE_AGE)

        true
      rescue Exception => e
        logger.debug("Layaway: Caught an exception in with_claim, with the coordination file locked: #{e.message}, #{e.backtrace.inspect}")
        raise
      ensure
        # Unlock the file when done!
        f.flock(File::LOCK_UN | File::LOCK_NB)
        f.close
      end
    else
      # Didn't obtain lock, another process is reporting. Return false from this function, but otherwise no work
      false
    end
  end
end

#write_reporting_period(reporting_period, files_limit = MAX_FILES_LIMIT) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/scout_apm/layaway.rb', line 52

def write_reporting_period(reporting_period, files_limit = MAX_FILES_LIMIT)
  if at_layaway_file_limit?(files_limit)
    # This will happen constantly once we hit this case, so only log the first time
    @wrote_layaway_limit_error_message ||= logger.error("Layaway: Hit layaway file limit. Not writing to layaway file")
    return false
  end
  logger.debug("Layaway: wrote time period: #{reporting_period.timestamp}")
  filename = file_for(reporting_period.timestamp)
  layaway_file = LayawayFile.new(context, filename)
  layaway_file.write(reporting_period)
rescue => e
  logger.debug("Layaway: error writing: #{e.message}, #{e.backtrace.inspect}")
  raise e
end