SubclassNotFound - single-table inheritance error due to unexpected class reloading?

I’m stumped on this one. I have a simple ActiveRecord model with a single-table inheritance field using the default type column:

class Entity < ApplicationRecord
  ...
end

class Door < Entity ; end
class Window < Entity ; end

Everything works fine as expected in development and in specs. However, when deployed and executed on Lambda, I get errors:

ActiveRecord::SubclassNotFound: Invalid single-table inheritance type: Window is not a subclass of Entity

I suspect this is related to the classes being reloaded somehow and referring to different instances of the Entity class? Not sure exactly. I’m not too clear how the Lambda execution context works and how Jets preloads models.

Any ideas or things to try?

Dug into this. Bit off a :rabbit::hole::joy::man_facepalming:t2:

Fortunately, was able to reproduce this locally without deploying onto Lambda.

What’s going on:

Classes are hot-reloaded in development mode, so we don’t have to restart the server every time there’s a code change.

Rails keeps a copy of the STI classes in memory. According to reports, STI and hot-reloading causes all sorts of woes. There’s even a hacky StiPreload module suggestion in the Rails Guide as a workaround.

Here’s where in ActiveRecord where the error gets triggered: https://github.com/rails/rails/blob/63107e9914c893336f7612c2cd17a24474b6a6d6/activerecord/lib/active_record/inheritance.rb#L243

Adding some debugging logs at that area of the code reveals the culprit.

          # ...
          puts "subclass: #{subclass}"
          puts "descendants #{descendants.inspect}"
          x = subclass == self
          y = descendants.include?(subclass)
          puts "subclass == self #{x}"
          puts "descendants.include?(subclass) #{y}"
          puts "subclass.object_id: #{subclass.object_id}"
          puts "descendants object_ids: #{descendants.map(&:object_id)}"

          unless subclass == self || descendants.include?(subclass)
            raise SubclassNotFound, "Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}"
          end

Hitting the server the first time will not produce the error. The error surfaces on the 2nd request after reloading has taken place. Here’s the log for both requests:

Logs from 2 requests
environment/demo (master) tung $ bundle exec jets server --host 0.0.0.0
=> bundle exec rackup --port 8888 --host 0.0.0.0
Jets booting up in development mode!
Puma starting in single mode...
* Version 4.3.1 (ruby 2.5.6-p201), codename: Mysterious Traveller
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://0.0.0.0:8888
Use Ctrl-C to stop
I, [2019-12-09T18:42:02.195710 #26651]  INFO -- : Started GET "/entities" for 11.22.33.44 at 2019-12-09 18:42:02 +0000
I, [2019-12-09T18:42:02.195770 #26651]  INFO -- : Processing EntitiesController#index
I, [2019-12-09T18:42:02.195870 #26651]  INFO -- :   Event: {"resource":"/entities","path":"/entities","httpMethod":"GET","headers":{"Version":"HTTP/1.1","Host":"localhost:8888","Connection":"keep-alive","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36","Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3","Accept-Encoding":"gzip, deflate","Accept-Language":"en-US,en;q=0.9,pt;q=0.8","Cookie":"_ga=GA1.2.256701190.1559426359; _ga=GA1.3.256701190.1559426359; _gid=GA1.2.1898455522.1575910316","cache-control":"max-age=0","upgrade-insecure-requests":"1"},"queryStringParameters":{},"pathParameters":null,"stageVariables":null,"requestContext":{},"body":null,"isBase64Encoded":false}
I, [2019-12-09T18:42:02.195925 #26651]  INFO -- :   Parameters: {}
subclass: Window
descendants [Window(id: integer, title: string, type: string, created_at: datetime, updated_at: datetime)]
subclass == self false
descendants.include?(subclass) true
subclass.object_id: 47233686373920
descendants object_ids: [47233686373920]
I, [2019-12-09T18:42:02.230049 #26651]  INFO -- : Completed Status Code 200 in 0.03438908s
11.22.33.44 - - [09/Dec/2019:18:42:02 +0000] "GET /entities HTTP/1.1" 200 - 0.1341
I, [2019-12-09T18:42:04.416450 #26651]  INFO -- : Started GET "/entities" for 11.22.33.44 at 2019-12-09 18:42:04 +0000
I, [2019-12-09T18:42:04.416529 #26651]  INFO -- : Processing EntitiesController#index
I, [2019-12-09T18:42:04.416618 #26651]  INFO -- :   Event: {"resource":"/entities","path":"/entities","httpMethod":"GET","headers":{"Version":"HTTP/1.1","Host":"localhost:8888","Connection":"keep-alive","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36","Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3","Accept-Encoding":"gzip, deflate","Accept-Language":"en-US,en;q=0.9,pt;q=0.8","Cookie":"_ga=GA1.2.256701190.1559426359; _ga=GA1.3.256701190.1559426359; _gid=GA1.2.1898455522.1575910316","cache-control":"max-age=0","upgrade-insecure-requests":"1"},"queryStringParameters":{},"pathParameters":null,"stageVariables":null,"requestContext":{},"body":null,"isBase64Encoded":false}
I, [2019-12-09T18:42:04.416693 #26651]  INFO -- :   Parameters: {}
subclass: Window
descendants [Window(id: integer, title: string, type: string, created_at: datetime, updated_at: datetime)]
subclass == self false
descendants.include?(subclass) false
subclass.object_id: 47233686373920
descendants object_ids: [70120615995860]
ActiveRecord::SubclassNotFound: Invalid single-table inheritance type: Window is not a subclass of Entity
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/inheritance.rb:256:in `find_sti_class'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/inheritance.rb:220:in `discriminate_class_for_record'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/persistence.rb:257:in `instantiate'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/querying.rb:58:in `block (2 levels) in find_by_sql'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/result.rb:62:in `block in each'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/result.rb:62:in `each'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/result.rb:62:in `each'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/querying.rb:58:in `map'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/querying.rb:58:in `block in find_by_sql'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activesupport-6.0.1/lib/active_support/notifications/instrumenter.rb:24:in `instrument'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/querying.rb:56:in `find_by_sql'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation.rb:810:in `block in exec_queries'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation.rb:828:in `skip_query_cache_if_necessary'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation.rb:797:in `exec_queries'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation.rb:615:in `load'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation.rb:250:in `records'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation/delegation.rb:85:in `as_json'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activesupport-6.0.1/lib/active_support/json/encoding.rb:35:in `encode'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activesupport-6.0.1/lib/active_support/json/encoding.rb:22:in `encode'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activesupport-6.0.1/lib/active_support/core_ext/object/json.rb:42:in `to_json'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/actionpack-6.0.1/lib/action_controller/metal/renderers.rb:157:in `block in <module:Renderers>'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/actionpack-6.0.1/lib/action_controller/metal/renderers.rb:150:in `block in _render_to_body_with_renderer'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/2.5.0/set.rb:338:in `each_key'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/2.5.0/set.rb:338:in `each'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/actionpack-6.0.1/lib/action_controller/metal/renderers.rb:146:in `_render_to_body_with_renderer'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/actionpack-6.0.1/lib/action_controller/metal/renderers.rb:142:in `render_to_body'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/actionpack-6.0.1/lib/abstract_controller/rendering.rb:46:in `render_to_string'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/actionpack-6.0.1/lib/action_controller/metal/rendering.rb:41:in `render_to_string'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/actionpack-6.0.1/lib/action_controller/renderer.rb:96:in `render'
        /home/ec2-user/tongueroo/jets/lib/jets/controller/rendering/rack_renderer.rb:37:in `render'
        /home/ec2-user/tongueroo/jets/lib/jets/controller/rendering.rb:29:in `render'
        /home/ec2-user/environment/demo/app/controllers/entities_controller.rb:12:in `index'
        /home/ec2-user/tongueroo/jets/lib/jets/controller/base.rb:83:in `dispatch!'
        /home/ec2-user/tongueroo/jets/lib/jets/controller/middleware/main.rb:27:in `call!'
        /home/ec2-user/tongueroo/jets/lib/jets/controller/middleware/main.rb:22:in `call'
        /home/ec2-user/tongueroo/jets/lib/jets/controller/middleware/main.rb:49:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/bundler/gems/webpacker-1a012c92ef7b/lib/webpacker/dev_server_proxy.rb:18:in `perform_request'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-proxy-0.6.5/lib/rack/proxy.rb:57:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/etag.rb:25:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/conditional_get.rb:25:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/head.rb:12:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/session/abstract/id.rb:232:in `context'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/session/abstract/id.rb:226:in `call'
        /home/ec2-user/tongueroo/jets/lib/jets/controller/middleware/local.rb:40:in `call'
        /home/ec2-user/tongueroo/jets/lib/jets/controller/middleware/reloader.rb:12:in `block in call'
        /home/ec2-user/tongueroo/jets/lib/jets/controller/middleware/reloader.rb:9:in `synchronize'
        /home/ec2-user/tongueroo/jets/lib/jets/controller/middleware/reloader.rb:9:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/method_override.rb:22:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/runtime.rb:22:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/shotgun-0.9.2/lib/shotgun/static.rb:14:in `call'
        /home/ec2-user/tongueroo/jets/lib/jets/middleware.rb:7:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/tempfile_reaper.rb:15:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/lint.rb:49:in `_call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/lint.rb:37:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/show_exceptions.rb:23:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/common_logger.rb:33:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/chunked.rb:54:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/content_length.rb:15:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/puma-4.3.1/lib/puma/configuration.rb:228:in `call'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/puma-4.3.1/lib/puma/server.rb:681:in `handle_request'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/puma-4.3.1/lib/puma/server.rb:472:in `process_client'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/puma-4.3.1/lib/puma/server.rb:328:in `block in run'
        /home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/puma-4.3.1/lib/puma/thread_pool.rb:134:in `block in spawn_thread'
subclass: Window
descendants [Window(id: integer, title: string, type: string, created_at: datetime, updated_at: datetime)]
subclass == self false
descendants.include?(subclass) false
subclass.object_id: 47233686373920
descendants object_ids: [70120615995860]
2019-12-09 18:42:04 +0000: Rack app error handling request { GET /entities }
#<ActiveRecord::SubclassNotFound: Invalid single-table inheritance type: Window is not a subclass of Entity>
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/inheritance.rb:256:in `find_sti_class'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/inheritance.rb:220:in `discriminate_class_for_record'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/persistence.rb:257:in `instantiate'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/querying.rb:58:in `block (2 levels) in find_by_sql'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/result.rb:62:in `block in each'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/result.rb:62:in `each'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/result.rb:62:in `each'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/querying.rb:58:in `map'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/querying.rb:58:in `block in find_by_sql'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activesupport-6.0.1/lib/active_support/notifications/instrumenter.rb:24:in `instrument'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/querying.rb:56:in `find_by_sql'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation.rb:810:in `block in exec_queries'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation.rb:828:in `skip_query_cache_if_necessary'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation.rb:797:in `exec_queries'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation.rb:615:in `load'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation.rb:250:in `records'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation.rb:245:in `to_ary'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation/finder_methods.rb:508:in `find_take_with_limit'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation/finder_methods.rb:98:in `take'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/activerecord-6.0.1/lib/active_record/relation.rb:708:in `inspect'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/show_exceptions.rb:104:in `inspect'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/show_exceptions.rb:104:in `h'
(erb):252:in `block in pretty'
(erb):249:in `each'
(erb):249:in `pretty'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/2.5.0/erb.rb:876:in `eval'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/2.5.0/erb.rb:876:in `result'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/show_exceptions.rb:96:in `pretty'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/show_exceptions.rb:32:in `rescue in call'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/show_exceptions.rb:22:in `call'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/common_logger.rb:33:in `call'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/chunked.rb:54:in `call'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/rack-2.0.7/lib/rack/content_length.rb:15:in `call'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/puma-4.3.1/lib/puma/configuration.rb:228:in `call'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/puma-4.3.1/lib/puma/server.rb:681:in `handle_request'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/puma-4.3.1/lib/puma/server.rb:472:in `process_client'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/puma-4.3.1/lib/puma/server.rb:328:in `block in run'
/home/ec2-user/.rbenv/versions/2.5.6/lib/ruby/gems/2.5.0/gems/puma-4.3.1/lib/puma/thread_pool.rb:134:in `block in spawn_thread'

Here’s a summary of the logs.

1st request:

subclass: Window
descendants [Window(id: integer, title: string, type: string, created_at: datetime, updated_at: datetime)]
subclass == self false
descendants.include?(subclass) true
subclass.object_id: 47233686373920
descendants object_ids: [47233686373920]

2nd request:

subclass: Window
descendants [Window(id: integer, title: string, type: string, created_at: datetime, updated_at: datetime)]
subclass == self false
descendants.include?(subclass) false
subclass.object_id: 47233686373920
descendants object_ids: [70120615995860]

We can see that even though Window is in the descendants array, it is not the same Window class. The object ids have changed. Jets reloads the class definitions due to eager loading. So the object_id is out of sync with the ActiveRecord descendants object_id reference. ActiveRecord then thinks that Window is not a subclass of Entity and then we get the error:

ActiveRecord::SubclassNotFound: Invalid single-table inheritance type: Window is not a subclass of Entity

Essentially, what’s going on is that Jets auto-reloads but keeps the same object_id and Rails ActiveRecord reloads and changes the object_id. This confuses and breaks the Rails STI lookup. To fix this STI reload issue, Jets uses Zeitwerk::Loader.eager_load_all now:

This will reload all included gems that also uses Zeitwerk for autoloading, which Rails thankfully uses nowadays.

There are some cavaets with the fix. Mainly the gems that use Zeitwerk must also be using Zeitwerk properly. One of my own gems actually had a slight issue so fixed it also as a part of this: https://github.com/tongueroo/aws-mfa-secure/pull/2

Useful links:

Bummer. So the fix above does not work. All that did was actually disabled auto-reloading for Jets :joy:

Will have to dig into another fix. Unsure how to handle at the moment though. :thinking:

Wow, @tung you are a hero!
Thank you so much for jumping on this quickly.

It’s interesting that you say that it’s a development reloading issue. Is it running in development mode when deployed to Lambda? Would just disabling auto-reloading on Lambda fix it? I can’t think of why we would need auto-reloading in production because the code wouldn’t change.

RE: Is it running in development mode when deployed to Lambda?

Hot reloading is only turned on in development mode. However, it is possible to deploy development mode to AWS Lambda.

If you have deployed with just jets deploy, then it will be development. If you have deployed with JETS_ENV=production jets deploy, then it’ll be production.

RE: Would just disabling auto-reloading on Lambda fix it?

Ah, missing the forrest from the trees. Yup, that’s a quick and easy fix. Added an option to do that.

Released in v2.3.8

You can probably set it based on an ENV var that is only available on AWS Lambda. https://docs.aws.amazon.com/lambda/latest/dg/lambda-environment-variables.html IE: _HANDLER or AWS_LAMBDA_FUNCTION_NAME, etc. Example:

config/application.rb

Jets.application.configure do
  config.hot_reload = Jets.env.development? && !ENV['_HANDLER']
  # ...
end

Also, this is just a quick fix. Would like to generally fix hot-reloading for STI. It’s pretty non-ideal to have to restart the server locally while developing.

Thinking may have to figure out how ActiveRecord is storing the references to the descendants and fix the reloading? It seems like ActiveRecord reload is changing the object_id. Unsure on approach though. :thinking:

Great. Disabling hot_reload on Lambda fixed it!
I can’t see any reason why hot reloading on Lambda would be useful, so I don’t see any downside to this. Would be nice if reloading worked right locally, too – but I can work around that.

RE: I can’t see any reason why hot reloading on Lambda would be useful, so I don’t see any downside to this.

Good call. Was being lazy with the fix. Will update Jets so it won’t reload on Lambda even in development mode.

config.hot_reload = !ENV['_HANDLER'] fails if the non-lambda environment is not named development. It would be better if the hot reload feature could be enabled/disabled in any env explicitly regardless of the name. My teams local development environment is not called development but we have an AWS deployment that is called development. So this feature has bit us twice, once in the AWS deployment and again in our local env. So we are unable to use the hot deploy feature in our development.

Hi, I came across the same problem you had and I managed to get around it by disabling the hot_reload setting in the development environment, but I would like to know if there is a definitive solution that allows me to keep hot_reload