Class: EducationForm::CreateDailySpoolFiles

Inherits:
Object
  • Object
show all
Includes:
SentryLogging, Sidekiq::Job
Defined in:
app/sidekiq/education_form/create_daily_spool_files.rb

Constant Summary collapse

MAX_RETRIES =
5
WINDOWS_NOTEPAD_LINEBREAK =
"\r\n"
STATSD_KEY =
'worker.education_benefits_claim'
STATSD_FAILURE_METRIC =
"#{STATSD_KEY}.failed_spool_file".freeze
LIVE_FORM_TYPES =
%w[1990 1995 1990e 5490 1990n 5495 0993 0994 10203 1990S].map { |t| "22-#{t.upcase}" }.freeze
AUTOMATED_DECISIONS_STATES =
[nil, 'denied', 'processed'].freeze

Instance Method Summary collapse

Methods included from SentryLogging

#log_exception_to_sentry, #log_message_to_sentry, #non_nil_hash?, #normalize_level, #rails_logger

Instance Method Details

#format_application(data, rpo: 0) ⇒ Object

Previously there was data.saved_claim.valid? check but this was causing issues for forms when

  1. on submission the submission data is validated against vets-json-schema

  2. vets-json-schema is updated in vets-api

  3. during spool file creation the schema is then again validated against vets-json-schema

  4. submission is no longer valid due to changes in step 2



159
160
161
162
163
164
165
166
# File 'app/sidekiq/education_form/create_daily_spool_files.rb', line 159

def format_application(data, rpo: 0)
  form = EducationForm::Forms::Base.build(data)
  track_form_type("22-#{data.form_type}", rpo)
  form
rescue => e
  inform_on_error(data, rpo, e)
  nil
end

#format_records(grouped_data) ⇒ Object

Convert the records into instances of their form representation. The conversion into ‘spool file format’ takes place here, rather than when we’re writing the files so we can hold the connection open for a shorter period of time.



79
80
81
82
83
84
85
86
87
88
# File 'app/sidekiq/education_form/create_daily_spool_files.rb', line 79

def format_records(grouped_data)
  raw_groups = grouped_data.each do |region, v|
    region_id = EducationFacility.facility_for(region:)
    grouped_data[region] = v.map do |record|
      format_application(record, rpo: region_id)
    end.compact
  end
  # delete any regions that only had malformed claims before returning
  raw_groups.delete_if { |_, v| v.empty? }
end

#group_submissions_by_region(records) ⇒ Object



71
72
73
# File 'app/sidekiq/education_form/create_daily_spool_files.rb', line 71

def group_submissions_by_region(records)
  records.group_by { |r| r.regional_processing_office.to_sym }
end

#inform_on_error(claim, region, error = nil) ⇒ Object



168
169
170
171
172
173
174
175
176
# File 'app/sidekiq/education_form/create_daily_spool_files.rb', line 168

def inform_on_error(claim, region, error = nil)
  StatsD.increment("#{STATSD_KEY}.failed_formatting.#{region}.22-#{claim.form_type}")
  exception = if error.present?
                FormattingError.new("Could not format #{claim.confirmation_number}.\n\n#{error}")
              else
                FormattingError.new("Could not format #{claim.confirmation_number}")
              end
  log_exception(exception, nil, send_email: false)
end

#performObject

Setting the default value to the ‘unprocessed` scope is safe because the execution of the query itself is deferred until the data is accessed by the code inside of the method.



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
# File 'app/sidekiq/education_form/create_daily_spool_files.rb', line 30

def perform
  retry_count = 0

  begin
    records = EducationBenefitsClaim
              .unprocessed.joins(:saved_claim).includes(:education_stem_automated_decision).where(
                saved_claims: {
                  form_id: LIVE_FORM_TYPES
                },
                education_stem_automated_decisions: { automated_decision_state: AUTOMATED_DECISIONS_STATES }
              )
    return false if federal_holiday?

    # Group the formatted records into different regions
    if records.count.zero?
      log_info('No records to process.')
      return true
    else
      log_info("Processing #{records.count} application(s)")
    end
    regional_data = group_submissions_by_region(records)
    formatted_records = format_records(regional_data)
    # Create a remote file for each region, and write the records into them
    writer = SFTPWriter::Factory.get_writer(Settings.edu.sftp).new(Settings.edu.sftp, logger:)
    write_files(writer, structured_data: formatted_records)
  rescue => e
    StatsD.increment("#{STATSD_FAILURE_METRIC}.general")
    if retry_count < MAX_RETRIES
      log_exception(DailySpoolFileError.new("Error creating spool files.\n\n#{e}
                                             Retry count: #{retry_count}. Retrying..... "))
      retry_count += 1
      sleep(10 * retry_count) # exponential backoff for retries
      retry
    else
      log_exception(DailySpoolFileError.new("Error creating spool files.
                                             Job failed after #{MAX_RETRIES} retries \n\n#{e}"))
    end
  end
  true
end

#write_files(writer, structured_data:) ⇒ Object

Write out the combined spool files for each region along with recording and tracking successful transfers. Creates or updates an SpoolFileEvent for tracking and to prevent multiple files per RPO per date during retries



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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'app/sidekiq/education_form/create_daily_spool_files.rb', line 93

def write_files(writer, structured_data:)
  structured_data.each do |region, records|
    region_id = EducationFacility.facility_for(region:)
    filename = "#{region_id}_#{Time.zone.now.strftime('%m%d%Y_%H%M%S')}_vetsgov.spl"
    spool_file_event = SpoolFileEvent.build_event(region_id, filename)

    if spool_file_event.successful_at.present?
      log_info("A spool file for #{region_id} on #{Time.zone.now.strftime('%m%d%Y')} was already created")
    else
      log_submissions(records, filename)
      # create the single textual spool file
      contents = records.map(&:text).join(EducationForm::CreateDailySpoolFiles::WINDOWS_NOTEPAD_LINEBREAK)

      begin
        writer.write(contents, filename)

        ## Testing to see if writer is the cause for retry attempt failures
        ## If we get to this message, it's not the writer object
        log_info("Successfully wrote to filename: #{filename}")

        # send copy of staging spool files to testers
        # This mailer is intended to only work for development, staging and NOT production
        # Rails.env will return 'production' on the development & staging servers and  which
        # will trip the unwary. To be safe, use ENV['HOSTNAME']
        email_staging_spool_files(contents) if
          # local developer development
          Rails.env.eql?('development') ||

          # VA Staging environment where we really want this to work.
          ENV['HOSTNAME'].eql?('staging-api.va.gov')

        # track and update the records as processed once the file has been successfully written
        track_submissions(region_id)

        records.each { |r| r.record.update(processed_at: Time.zone.now) }
        spool_file_event.update(number_of_submissions: records.count, successful_at: Time.zone.now)
      rescue => e
        StatsD.increment("#{STATSD_FAILURE_METRIC}.#{region_id}")
        attempt_msg = if spool_file_event.retry_attempt.zero?
                        'initial attempt'
                      else
                        "attempt #{spool_file_event.retry_attempt}"
                      end
        exception = DailySpoolFileError.new("Error creating #{filename} during #{attempt_msg}.\n\n#{e}")
        log_exception(exception, region)
        if spool_file_event.retry_attempt < MAX_RETRIES
          spool_file_event.update(retry_attempt: spool_file_event.retry_attempt + 1)

          # Reinstantiate the writer before retrying
          writer = SFTPWriter::Factory.get_writer(Settings.edu.sftp).new(Settings.edu.sftp, logger:)
          retry
        else
          next
        end
      end
    end
  end
ensure
  writer.close
end