Rails Asset Caching Breaks First few page loads - ruby-on-rails

We're using Rails asset caching for JS and CSS like this:
<%= stylesheet_link_tag 'reset','global','admins','autocomplete', 'date_input', 'tablesorter', 'partners', 'jqmodal', :media => 'screen', :cache => set_asset_cache(:admins) %>
<%= javascript_include_tag :defaults, 'autocomplete', 'searchbox', 'jqmodal', :cache => set_asset_cache(:admins) %>
In our deploy we call rake tmp:assets:clear each time. The problem is that the first few page loads after a deploy come up with no css or js on the page. I guess until the cached all.js and all.css have been regenerated.
We deploy many times per day and this is scary for any users who happen to come across a busted page.
Have people found any way to make this smoother so the new cached assets are guaranteed to be there on the first new page load?

The AssetHat gem addresses this exact problem. Instead of concatenating assets the first time a page is loaded (which increases that page's load time), it concatenates assets on deploy instead. As a bonus, the gem also minifies your CSS and JS, which saves precious bytes.
After setup, usage is pretty simple:
Use include_css :bundle => 'admins' and include_js :bundle => 'admins' in your layout. (The bundle contents are set in a config file to keep your layout lightweight.)
Add rake asset_hat:minify to your deploy script. My company has been using it in production with Capistrano for about a year now.
There's more info in the readme and docs, and I'd be happy to hear any questions/ideas!

You could try warming the cache during deployment using wget, as an example (shamelessly reposted):
wget -r -nd --delete-after http://whatever.com/~popular/page/
However, this would have to be executed after you switch your symlink to your new deployment. A possibly more elegant solution might be to call the asset caching methods manually in your deploy, though I'm not sure how feasible that is. Here's where the caching is performed in Rails:
# File vendor/rails/actionpack/lib/action_view/helpers/asset_tag_helper.rb, line 273
273: def javascript_include_tag(*sources)
274: options = sources.extract_options!.stringify_keys
275: concat = options.delete("concat")
276: cache = concat || options.delete("cache")
277: recursive = options.delete("recursive")
278:
279: if concat || (ActionController::Base.perform_caching && cache)
280: joined_javascript_name = (cache == true ? "all" : cache) + ".js"
281: joined_javascript_path = File.join(joined_javascript_name[/^#{File::SEPARATOR}/] ? ASSETS_DIR : JAVASCRIPTS_DIR, joined_javascript_name)
282:
283: unless ActionController::Base.perform_caching && File.exists?(joined_javascript_path)
284: write_asset_file_contents(joined_javascript_path, compute_javascript_paths(sources, recursive))
285: end
286: javascript_src_tag(joined_javascript_name, options)
287: else
288: expand_javascript_sources(sources, recursive).collect { |source| javascript_src_tag(source, options) }.join("\n")
289: end
290: end
You might be able to modify the caching code and run it manually on deploy.

Related

How to set up ActionCable with Importmaps in Rails 6? (MRI and Jruby)

Actioncable - an overview.
I am using jruby 9.2.16 (hence ruby 2.5.7) with rails 6.1.6.1.
I am not sure if only in development or only without ssl (wss) Actioncable can be used with the simple client side:
var ws = new WebSocket('ws://0.0.0.0:3000/channels');
ws.onmessage = function(e){ console.log(e.data); }
But at least I didn't get it running to do "Streaming from Channel" using wss in production, as it works locally (visible starting 'redis-cli' in terminal, and then 'monitor').
So I tried to implement the actioncable client scripts and hence 8 days got lost.
First I struggled with the fact that there is no description which is somehow complete. Many people publish particular solutions, but this is like gambling: maybe you are lucky.
Second, files are named in a way that seems to be general even though they are only about actioncable (the folder 'javascript', or 'application.js')
It is misleading to not call them 'actioncable_files' and 'actioncable_app.js' and voilà there are problems because of several files with the same name.
The next problem is, that lots has to be changed only, because the orderly structure of files is ignored. Javascripts no longer are in assets, but why?
They could be in assets/javascripts/actioncable/ ?
So manifest.js has to be changed, and even attributes have to be added in application.rb (attr_accessor :importmap). Something you find after some days in a forum.
but even more confusing: importmap is a gem, which requires some directories and which somehow has to be installed (rake app:update:bin, rails importmap:install) and someone wrote, the order of the gems were relevant but you cannot only deinstall actioncable gem, because rails depends on that. Dependencies could be organized using a permutation of priority.
importmaps is not working in firefox, so you need shims additionally
But in the end, everything what importmap does, looks like reinventing the wheel to me: it loads javascript files. Something, that can be done easily manually.
Also importing modules is possible in simple javascript. And what else then a javascript file shall it be, that the browser finally will work with?
Now importmap creates from a string '#rails/actioncable' another string 'actioncable.esm.js', which in my case (after 8 days of working 15 h to get it to work, still is not found automatically.
I cannot find that file, or any description where it is generated, or if it is only a link, or has to be compiled somehwere, but it looks to me, as if importmap is completely redundant and only makes things very complicated. I don't understand the benefit from writing a string 'xyz' which is translated in a laborious way to set up into another string 'xyz_2', which also might not be found. And if there are variables, they can be loaded directly using the same idea like action_cable_meta_tag.
Technically Actioncable is only doing what Faye did before. So why do we need all that so called "modern" way, which I think only is reinventing the wheel?
So, I would like to create a description on how to install actioncable in an easy way - without unnecessary tools and clearly.
But first I need to get it to work on my own. And therefore the question is: what shall I do due to:
GET http://0.0.0.0:3000/actioncable.esm.js net::ERR_ABORTED 404 (Not Found)
Thanks everyone for any idea!
I've never set up ActionCable before and never used it (except second hand through Turbo::Broadcastable). Seems like you had quite the journey. I used rails guides and rails github to set it up.
First of all, from importmap-rails:
Note: In order to use JavaScript from Rails frameworks like Action Cable, Action Text, and Active Storage, you must be running Rails 7.0+. This was the first version that shipped with ESM compatible builds of these libraries.
https://github.com/rails/importmap-rails#installation
Challenge accepted. I'll use mri for now (I'll try with your versions later, to see if anything weird comes up).
ActionCable
$ rails _6.1.6.1_ new cable --skip-javascript
$ cd cable
# https://github.com/rails/importmap-rails#installation
$ bin/bundle add importmap-rails
$ bin/rails importmap:install
# ActionCable guide seems rather crusty. Until this section:
# https://guides.rubyonrails.org/v6.1/action_cable_overview.html#connect-consumer
# A generator for the client side js is mentioned in the code comment.
$ bin/rails g channel chat
# Oops, generated server side rb as well.
# This should really be at the start of the guide.
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
# NOTE: just keep it simple
stream_from "some_channel"
end
end
The directory app/javascript seems generic, because it is. This is for all Javascript stuff, used by shakapacker, jsbundling-rails, importmap-rails and others. I've described it a bit here: https://stackoverflow.com/a/73174481/207090
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create("ChatChannel", {
connected() {
// NOTE: We have to check if our set up is online first,
// before chatting and authenticating or anything else.
console.log("ChatChannel connected")
},
disconnected() {},
received(data) {}
});
To broadcast a message, call this somewhere in your app [sic]:
ActionCable.server.broadcast("some_channel", "some message")
Ok, we have to make a controller anyway:
$ bin/rails g scaffold Message content
$ bin/rails db:migrate
$ open http://localhost:3000/messages
$ bin/rails s
Also, channels have to be imported somewhere, to load on the page. javascript_importmap_tags in the layout only imports application:
https://github.com/rails/importmap-rails#usage
<script type="module">import "application"</script>
<!-- ^ -->
<!-- this imports the pinned `application` -->
Makes sense to import channels in application.js. Can't import ./channels/index because it has require. We'd have to use node for it to work or do something else to import all the channels. Manual way is the simplest:
// app/javascript/channels/index.js
// NOTE: it works a little differently with importmaps that I haven't mentioned yet.
// skip this index file for now, and import channels in application.js
// app/javascript/application.js
import "./channels/chat_channel"
Browser console shows missing #rails/actioncable. Nobody told me to install it yet. Use pin command to add it:
https://github.com/rails/importmap-rails#using-npm-packages-via-javascript-cdns
$ bin/importmap pin #rails/actioncable
Refresh the browser:
ChatChannel connected chat_channel:5
We got our javascript on the page. Let's make it broadcast:
# app/controllers/messages_controller.rb
# POST /messages
def create
#message = Message.create(message_params)
ActionCable.server.broadcast("some_channel", #message.content)
end
<!-- app/views/messages/index.html.erb -->
<div id="chat"></div> <!-- output for broadcasted messages -->
<!-- since I have no rails ujs, for my purposes: bushcraft { remote: true } -->
<!-- v -->
<%= form_with model: Message.new, html: { onsubmit: "remote(event, this)" } do |f| %>
<%= f.text_field :content %>
<%= f.submit %>
<% end %>
<script type="text/javascript">
// you can skip this, I assume you have `rails_ujs` installed or `turbo`.
// { remote: true } or { local: false } is all you need on the form.
function remote(e, form) {
e.preventDefault();
fetch(form.action, {method: form.method, body: new FormData(form)})
form["message[content]"].value = ""
}
</script>
We know we're connected. The form is submitting to MessagesController#create without refreshing where we're broadcasting to "some_channel". All that's left is to do is output data on the page:
https://guides.rubyonrails.org/v6.1/action_cable_overview.html#client-server-interactions-subscriptions
// app/javascript/channels/chat_channel.js
// update received() function
received(data) {
document.querySelector("#chat")
.insertAdjacentHTML("beforeend", `<p>${data}</p>`)
}
ActionCable done. Now let's fix importmaps.
Importmaps
Something I didn't mention before and it is super important to understand.
Everything works, but, only in development, I explained why here:
https://stackoverflow.com/a/73136675/207090
URLs and relative or absolute paths will not be mapped and more importantly will bypass the asset pipeline sprockets. To actually use importmaps, all the files in app/javascript/channels have to be mapped, aka pinned, and then referred to only by the pinned name when importing.
# config/importmap.rb
# NOTE: luckily there is a command to help with bulk pins
pin_all_from "app/javascript/channels", under: "channels"
pin "application", preload: true
pin "#rails/actioncable", to: "https://ga.jspm.io/npm:#rails/actioncable#7.0.3-1/app/assets/javascripts/actioncable.esm.js"
# NOTE: the big reveal -> follow me ->------------------------------------------------------------------^^^^^^^^^^^^^^^^^^
# NOTE: this only works in rails 7+
# pin "#rails/actioncable", to: "actioncable.esm.js"
# `actioncable.esm.js` is in the asset pipeline so to speak and can be found here:
# https://github.com/rails/rails/tree/v7.0.3.1/actioncable/app/assets/javascripts
For some info on pin and pin_all_from:
https://stackoverflow.com/a/72855705/207090
You can see the importmaps this creates in the browser or in the terminal:
$ bin/importmap json
{
"imports": {
"application": "/assets/application-3ac17ae8a9bbfcdc9571d7ffac88746f5a76b18c149fdaf02fa7ed721b3e7c49.js",
"#rails/actioncable": "https://ga.jspm.io/npm:#rails/actioncable#7.0.3-1/app/assets/javascripts/actioncable.esm.js",
"channels": "/assets/channels/index-78e712d4a980790be34a2e859a2bd9a1121f9f3b508bd3f7de89889ff75828a0.js",
"channels/chat_channel": "/assets/channels/chat_channel-0a2f983da2629a4d7edef5b7f05a494670df3f99ec6a22a2e2fee91a5d1c1d05.js",
"channels/consumer": "/assets/channels/consumer-b0ce945e7ae055dba9cceb062a47080dd9c7794a600762c19d38dbde3ba8ff0d.js"
}# ^ ^
} # | |
# names you use urls browser uses
# | to import ^ to actually get it
# | |
# `---> importmaped to ----'
For importmaps info (not the importmap-rails gem):
https://github.com/WICG/import-maps
Importmaps do not import anything, they map name to url. If you make name look like url with /name, ./name, ../name, http://js.cdn/name there is nothing to map.
import "channels/chat_channel"
// stays unchanged and is now the same as
import "/assets/channels/chat_channel-0a2f983da2629a4d7edef5b7f05a494670df3f99ec6a22a2e2fee91a5d1c1d05.js"
// because we have an importmap for "channels/chat_channel"
You don't want to use the second form with an absolute path in you js files, because the digest hash changes on file updates to invalidate the browser cache (this is handled by sprockets).
Convert all the imports:
import consumer from "./consumer"
import "./channels/chat_channel"
to match the pinned names:
import consumer from "channels/consumer"
import "channels/chat_channel"
// import "channels" // is mapped to `channels/index`
// TODO: want to auto import channels in index file?
// just get all the pins named *_channel and import them,
// like stumulus-loading does for controllers:
// https://github.com/hotwired/stimulus-rails/blob/v1.1.0/app/assets/javascripts/stimulus-loading.js#L8
Jruby
Same setup on jruby. I just installed it and updated my Gemfile:
# Gemfile
ruby "2.5.7", engine: "jruby", engine_version: "9.2.16.0"
gem "activerecord-jdbcsqlite3-adapter"
gem "importmap-rails", "< 0.8" # after version 0.8.0 ruby >= 2.7 is required
First error, when starting the server:
NoMethodError: private method `importmap=' called for #<Cable::Application:0x496a31da>
importmap= method is defined here:
https://github.com/rails/importmap-rails/blob/v0.7.6/lib/importmap/engine.rb#L4
Rails::Application.send(:attr_accessor, :importmap)
In jruby it defines private methods when used this way:
>> A = Class.new
>> A.attr_accessor(:m)
>> A.new.m
NoMethodError (private method 'm' called for #<A:0x5f2f577>)
The fix is to override the definition in you app, or make those methods public:
# config/application.rb
module Cable
class Application < Rails::Application
# make them public
public :importmap, :importmap=
config.load_defaults 6.1
end
end
That's it. No other issues. You should expect some set backs anyway, because you're using jruby which is quite behind the mri. Ruby 2.5 EOL'd on Apr 05, 2021. You can't expect latest gems to play nice with old ruby versions.

Set System Directory Rails Production Environment

I have an app that works fine in on my development machine, but on my production server it uses a broken link to serve an image served using the Paperclip Gem.
Production environment is Linux(Debian), Apache, Passenger and I am deploying with Capistrano.
The app is stored in (a symlink that points to the public folder of the current version of the app deployed using capistrano):
/var/www/apps/root/appname
However, when I try and access it on the production server, the Apache error log displays this as the path it is looking in:
/var/www/apps/root/system
The correct path, however, is:
/var/www/apps/appname/shared/system
One option available to me is to create a symlink in root that directs system to the correct path, but I don't want to do this in case I want to deploy another app in the same root dir.
The url for this request is generated by rails, but Apache is what fetches the static resource (image files), so I have tried placing the following in my config/environments/production.rb:
ENV["RAILS_RELATIVE_URL_ROOT"] = '/appname/'
Which has resolved all other pathing issues I've been experiencing, but when rails generates the url (via the Paperclip gem), it doesn't seem to use it.
How can I set it so Paperclip uses the right path and only uses it production?
I've a workaround, add this as one of initializers:
config/initializer/paperclip.rb
Paperclip::Attachment.class_eval do
def url(style_name = default_style, options = {})
if options == true || options == false # Backwards compatibility.
res = #url_generator.for(style_name, default_options.merge(:timestamp => options))
else
res = #url_generator.for(style_name, default_options.merge(options))
end
# replace adding uri before res, minus final /
Rails.application.config.site_relative_url[0..-2]+res
end
end
At the moment Paperclip doesn’t work with ENV['RAILS_RELATIVE_URL_ROOT'] and the. You can follow the issue here:
https://github.com/thoughtbot/paperclip/issues/889

Rails how to delete a file without failing on error

I'm using JPEGCAM to allow users to take a profile pic with their web cam. This uploads a temporary file as so:
def ajax_photo_upload
File.open(upload_path, 'w:ASCII-8BIT') do |f|
f.write request.raw_post
end
# #user.photo = File.open(upload_path)
#user.assign_attributes(
:photo => File.open(upload_path),
:orig_filename => "#{current_user.full_name}.jpg"
)
if #user.save
respond_to do |format|
.....
private
def upload_path # is used in upload and create
file_name = session[:session_id].to_s + '.jpg'
File.join(::Rails.root.to_s, 'public', 'temp', file_name)
end
What's the best way to go about deleting this temporary file safely? Thanks
When you know that you are done with the file:
File.delete(path_to_file) if File.exist?(path_to_file)
Another thing: make sure that you always close files that you have opened, an operating system can only handle a certain number of open files/file descriptors and you'll may run into strange bugs when you pass that limit... So when you want to open files in Ruby always either use the block form:
File.open(path) do |f|
# ...
end
and Ruby will close the file automatically for you. If the block form is not usable, you have to close files by yourself:
f = File.open(path)
# ...
f.close
So make sure to close the file that you pass to #user.assign_attributes(...)...
If you are sure you are done with it, why not just use FileUtils.rm or FileUtils.rm_f?
FileUtils.rm_f(upload_path)
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/fileutils/rdoc/FileUtils.html#method-c-rm_f
You could also ignore this in Rails, and have a cron that wakes up and deletes files older than a day from the temp directory that match these temp files. That has the benefit of some margin for error if a file fails to be reprocessed - you don't rm it immediately - and the file operation is not done on the request/response loop for Rails, which will then respond a bit faster.

Dynamic CSS using Sprockets

I'm working on a site builder in rails and I would like to render the sites css using Sprockets SCSS processors. Since the user can change colors and logos, I can't use Sprockets precompilation so I've started working on a Rails SCSS template handler to handle dynamic views.
The goal is to compile 'app/views/sites/show.css.scss' any time /sites/43543.css is requested. Here's what I have so far. You'll notice I first run the template through the ERB processor and then attempt to run it through Sprockets.
https://gist.github.com/3870095
Manuel Meurer came up with an alternative solution that writes the ERB output to a path and then triggers the Asset Pipeline to compile it. I was able to get his solution to work locally but it wont work on heroku because the asset path is not writable. Files can only be written to the tmp directory and those files are only guaranteed for a single request.
http://www.krautcomputing.com/blog/2012/03/27/how-to-compile-custom-sass-stylesheets-dynamically-during-runtime/
After a long day I was able to solve my problem thanks to John Feminella and his post on google. The challenging part for me was figuring out how to create a new Sprockets::Context. Luckily John's solution doesn't require a Context.
Updated gist here
Attempt #1
This code does not allow importing css files from the asset pipeline.
#import "foundation"; works because foundation is loaded as a compass module
#import "custom_css"; results in an error message saying the file could not be found
def call(template)
erb = ActionView::Template.registered_template_handler(:erb).call(template)
%{
options = Compass.configuration.to_sass_engine_options.merge(
:syntax => :scss,
:custom => {:resolver => ::Sass::Rails::Resolver.new(CompassRails.context)},
)
Sass::Engine.new((begin;#{erb};end), options).render
}
end
Attempt #2
This code fails to embed base64 urls using asset-data-url
def call(template)
erb = ActionView::Template.registered_template_handler(:erb).call(template)
%{
compiler = Compass::Compiler.new *Compass.configuration.to_compiler_arguments
options = compiler.options.merge({
:syntax => :scss,
:custom => {:resolver => ::Sass::Rails::Resolver.new(CompassRails.context)},
})
Sass::Engine.new((begin;#{erb};end), options).render
}
end
Attempt 3
Turns out you can use empty values while creating the context. Code below works in development but throws an error in production.
ActionView::Template::Error (can't modify immutable index)
It appears the error occurs in Sprockets::Index which is used instead of Sprockets::Environment in production. Switching to Sprockets::Environment doesn't solve the problem either.
def call(template)
erb = ActionView::Template.registered_template_handler(:erb).call(template)
%{
context = CompassRails.context.new(::Rails.application.assets, '', Pathname.new(''))
resolver = ::Sass::Rails::Resolver.new(context)
compiler = Compass::Compiler.new *Compass.configuration.to_compiler_arguments
options = compiler.options.merge({
:syntax => :scss,
:custom => {:resolver => resolver}
})
Sass::Engine.new((begin;#{erb};end), options).render
}
end

Conditional javascript require in the asset pipeline

I'm struggling with the asset pipeline. I'm loading dojo from Google CDN putting this in my template:
= javascript_include_tag 'http://ajax.googleapis.com/ajax/libs/dojo/1.6.1/dojo/dojo.xd.js', :'data-dojo-config' => %Q(dojoBlankHtmlUrl:'/blank.html', baseUrl: 'assets/', modulePaths: {custom: 'javascripts/modules'})
I just want a fallback to a local version if running locally or if the CDN is down. I thought of doing this:
script typeof(dojo) === "undefined" && document.write(unescape('%3Cscript src="js/libs/dojo-1.6.1.min.js"%3E%3C/script%3E'));
But I don't like it as it works out of the asset pipeline. I want to keep dojo in vendors/assets/javascripts/dojo. How can I get the fallback to be served by the asset pipeline.
Is there a way do declare conditional require in the asset pipeline. What I want is to run some javascript tests, and depending on the result serve a file.
Thanks
I suggest you use yepnope, a lightweight library for loading libraries like this in parallel (for speed) and it gives you the option to run some other code to test if the library is loaded. For example:
yepnope([{
load: 'http://ajax.googleapis.com/ajax/libs/dojo/1.6.1/dojo/dojo.xd.js',
complete: function () {
if (!window.jQuery) {
yepnope('asset_path('you_local_copy_of_dojo') ');
}
}
}])
(Note: You will need erb tags around the asset_path helper)
The local dojo file would be in the assets/javascript folder, but not included in the application manifest. You need to add the dojo file to the precompile array:
config.assets.precompile += 'your_local_file.js'
And this will make it available to the asset_path helper.
Thanks Richard!
I don't want to have yepnope to load one library. It would be overkill imo. Here is the solution I came up with, based on your help (written in slim):
1/ In vendors/assets/javascripts/, I have my dojo.js.
2/ In config/application.rb:
# Precompile these assets files
config.assets.precompile += ['dojo.js']
3/ In the template:
= javascript_include_tag "http://ajax.googleapis.com/ajax/libs/dojo/#{Settings.dojoVersion}/dojo/dojo.xd.js", :'data-dojo-config' => %Q(dojoBlankHtmlUrl:'/blank.html', baseUrl: 'assets/', modulePaths: {custom: 'javascripts/modules'})
script = "typeof(dojo) === \"undefined\" && document.write(unescape('%3Cscript src=\"#{asset_path('dojo')}\"%3E%3C/script%3E'));".html_safe
I also posted on the Rails Google Group to request the addition of two options to the javascript_include_tag, :test and :local that would take care of all the work. We'll see.

Resources