rubyonjets / jets

Ruby on Jets
http://rubyonjets.com
MIT License
2.6k stars 181 forks source link

undefined method `read' for nil:NilClass when calling any endpoint #724

Closed enmachs closed 5 months ago

enmachs commented 6 months ago

Checklist

My Environment

Software Version
Operating System
Jets 5.0.13
Ruby 3.2.2

Expected Behaviour

Every call to the API should return an ok status even if lambdas

Current Behavior

Calling the api endpoint using api gateway tester:

image

Raises this error in the export controller lambda:

NoMethodError (undefined method `read' for nil:NilClass):

app/controllers/export_controller.rb:26:in `update_data'
app/controllers/export_controller.rb:3:in `create'
app/controllers/ExportController.rb:1:in `run'

I was having the same error when deploying but was fixed here

I have found some reference pointing to this method on jets, and looks invoke method for lambda client comes with payload nil or unavailable for some reason.

{
    "errorMessage": "undefined method `read' for nil:NilClass",
    "errorType": "Function<NoMethodError>",
    "stackTrace": [
        "/opt/ruby/gems/3.2.0/gems/jets-5.0.12/lib/jets/commands/call/caller.rb:78:in `remote_run'",
        "/opt/ruby/gems/3.2.0/gems/jets-5.0.12/lib/jets/commands/call/caller.rb:31:in `run'",
        "/opt/ruby/gems/3.2.0/gems/jets-5.0.12/lib/jets/job/base.rb:39:in `perform_later'",
        "/var/task/app/jobs/process_job.rb:30:in `block in aggregate_data'",
        "/opt/ruby/gems/3.2.0/gems/activerecord-7.0.8.1/lib/active_record/relation/delegation.rb:88:in `each'",
        "/opt/ruby/gems/3.2.0/gems/activerecord-7.0.8.1/lib/active_record/relation/delegation.rb:88:in `each'",
        "/var/task/app/jobs/process_job.rb:29:in `aggregate_data'",
        "/var/task/app/jobs/process_job.rb:36:in `story_aggregate_process'",
        "/var/task/app/jobs/process_job.rb:16:in `process_one_story'",
        "/opt/ruby/gems/3.2.0/gems/jets-5.0.12/lib/jets/job/base.rb:28:in `process'",
        "/opt/ruby/gems/3.2.0/gems/jets-5.0.12/lib/jets/exception_reporting.rb:37:in `block in process'",
        "/opt/ruby/gems/3.2.0/gems/jets-5.0.12/lib/jets/exception_reporting.rb:8:in `with_exception_reporting'",
        "/opt/ruby/gems/3.2.0/gems/jets-5.0.12/lib/jets/exception_reporting.rb:36:in `process'",
        "app/jobs/process_job.rb:1:in `run'",
        "/opt/ruby/gems/3.2.0/gems/jets-5.0.12/lib/jets/processors/main_processor.rb:32:in `instance_eval'",
        "/opt/ruby/gems/3.2.0/gems/jets-5.0.12/lib/jets/processors/main_processor.rb:32:in `run'",
        "/opt/ruby/gems/3.2.0/gems/jets-5.0.12/lib/jets/core.rb:171:in `process'",
        "/var/task/handlers/jobs/process_job.rb:9:in `process_one_story'"
    ]
}

Step-by-step reproduction instructions

Code Sample

export_controller.rb:

class ExportController < ApplicationController
  def create
    export_params[:update_data].present? ? update_data : export_csv

    render json: { message: "Export and Updating Data in progress" }
  end

  private

  def export_params
    params.permit(:story_id, :locale, :email, :update_data, :update_chart_data)
  end

  def export_csv
    ExportJob.perform_later(:export, { story_id: export_params[:story_id],
                                       locale: export_params[:locale],
                                       email: export_params[:email] })
  end

  def update_data
    ProcessJob.perform_later(:process_one_story, { story_id: export_params[:story_id],
                                                   locale: export_params[:locale],
                                                   email: export_params[:email] })
  end
end

process_job.rb

# frozen_string_literal: true

class ProcessJob < ApplicationJob
  class_managed_iam_policy(
    'service-role/AWSLambdaBasicExecutionRole'
  )
  def process_data
    Story.aggregated_viewings.find_each do |story|
      story_aggregate_process(story)
    end
  end

  def process_one_story
    story_id = event["story_id"]
    story = Story.includes(:viewings).find_by(id: story_id)
    story_aggregate_process(story)
    { message: "Data processing story: #{story_id} - done" }
  end

  private

  def remove_data(story)
    DataAggregation.where(story_id: story).delete_all
    story.viewings.where(started: true).update_all(aggregated: false)
    story.update_attribute(:update_csv, false)
  end

  def aggregate_data(story)
    viewings = story.viewings
    viewings.where(aggregated: false, started: true).each do |viewing|
      AggregateJob.perform_later(:aggregate_data, { story: story.id, viewing: viewing.id })
    end
  end

  def story_aggregate_process(story)
    remove_data(story) if story.update_csv
    aggregate_data(story)
  end
end

aggregate_job.rb

# frozen_string_literal: true

class AggregateJob < ApplicationJob
  class_reserved_concurrent_executions 10

  def aggregate_data
    perform_data(event)
  end

  private

  def perform_data(event)
    story = Story.find_by(id: event["story"])
    viewing = Viewing.find_by(id: event["viewing"])

    aggregate_data_for_language(story, viewing)
    viewing.update_attribute(:aggregated, true)
  end

  def aggregate_data_for_language(story, viewing)
    story.transcription_languages.each do |language|
      csv_row, single_row, multi_row = Export::Answers.new(story: story, language: language, viewing: viewing ).generate_csv_line
      data_aggregation = build_update_attributes(viewing, language, csv_row, single_row, multi_row, story)
      DataAggregation.update_or_create_by({ viewing_id: viewing.id, locale: language }, data_aggregation)
    end
  end
  def build_update_attributes(viewing, language, csv_row, single_row, multi_row, story)
    {
      story_id: story.id,
      viewing_id: viewing.id,
      data_order: viewing.start_time,
      multi_choice_data: multi_row.to_csv,
      single_choice_data: single_row.to_csv,
      data: csv_row.to_csv,
      locale: language
    }
  end
end

config/routes.rb

Jets.application.routes.draw do
  root "jets/welcome#index"

  get 'aggregate', to: 'aggregate#process'
  resources :export, only: [:create]

  # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
  # Can be used by load balancers and uptime monitors to verify that the app is live.
  get "up" => "jets/health#show", as: :jets_health_check

  # The jets/public#show controller can serve static utf8 content out of the public folder.
  # Note, as part of the deploy process Jets uploads files in the public folder to s3
  # and serves them out of s3 directly. S3 is well suited to serve static assets.
  # More info here: https://rubyonjets.com/docs/extras/assets-serving/
  any "*catchall", to: "jets/public#show"
end

config/application.rb

module ExportData
  class Application < Jets::Application
    config.load_defaults 5.0

    config.project_name = "export-data"
    config.mode = "api"

    config.prewarm.enable = true

    config.controllers.default_protect_from_forgery = false

    if Jets.env == 'production'
      config.function.vpc_config = {
        security_group_ids: ENV['SECURITY_GROUP_IDS'].split(','),
        subnet_ids: ENV['SUBNET_IDS'].split(',')
      }
    end
    # Docs:
    # https://rubyonjets.com/docs/config/
    # https://rubyonjets.com/docs/config/reference/
  end
end

Solution Suggestion

Since it looks it's for logging purpose, we could validate response would be validate payload it's not nil before trying to access it.

tongueroo commented 5 months ago

Don't think this applies anymore with the release of Jets 6