before(:each)doallow_any_instance_of(App::BusinessClass::Internal).toreceive(:call)do|instance,first_argument|["SUCCESS"]endend
If you read the RSpec-Mocks documentation about that rspec-mock method you’ll see that the developers of RSpec don’t recommend its usage. Probably they are right maybe you have a bad design or your test is trying to do too much or the object you’re testing is too complex.
If you think you’re testing the right think and you don’t find a better design I think the proper way to provide a substitute implementation is using stub_const. The previous code can be written as
classApp::BusinessClass::InternalMock<App::BusinessClass::InternalMockdefcall(arg)["SUCCESS"]endendbefore(:each)dostub_const"App::BusinessClass::Internal",App::BusinessClass::InternalMockend
This can be useful to provide alternative implementation of external API consumers or to speed up some specs as we’re doing with Paperclip
You’ve just read about RSpec stub_const to bypass APIs on Development, sports, and more by @PacoGuzman.
If you’d prefer to receive your updates in tweet form, please follow me on Twitter, otherwise I hope you’re enjoying the feed!
]]>These application logs are managed by a logstash instance that we use to gather some metrics, store all the events logs to a elasticsearch instance and some other things. Our Rails apps already use NewRelic so we have the default NewRelic features by default in all our apps this includes server side metrics and browser/user metrics. But one of the problems of NewRelic is its retention policy and lack of flexibility in its free plan.
That’s why as we have all our apps log events already in elasticsearch we decided to build our own browser metrics using our existing infrastructure. I forgot it but we are sending some of our metrics to Librato from our logstash instance too.
To-do this we implemented a Rails engine that is mounted in each Rails app which browser metrics we want to measure, basically the engine is form by:
A piece of javascript that using the Navigation Timing API sends browser information on each request to the application itself.
A controller that will log the information that we get from the user browser and as we use lograge will be processed as any other request.
The javascript code retrieve the Navigation Timing information when a new page is loaded and on the onload
window event send that information on an AJAX post request to the url where we mounted the engine.
window.addEventListener("DOMContentLoaded",function(){vardata=window.performance.timing;url='/_stats',// where you mounted the enginerequest=newXMLHttpRequest();request.open('POST',url,true);request.setRequestHeader('Content-Type','application/x-www-form-urlencoded; charset=UTF-8');request.send(data);},false);
And the controller logs all that information as a normal requests.
2015-03-15T06:33:40+00:00 production-web06 app-movida: method=POST path=/_rum/stats format=*/* controller=bebanjo_rum/stats action=create status=200duration=3.10 db=0.67 timestamp=2015-03-15T06:33:40+00:00 uuid=C17125076B4E_0A23992001BB_550527C2_XXXX794EC9 client_ip=XXX.XXX.XX.X params={"timing":{"crossBrowserLoadEvent":"1426401219807","firstPaint":"0","loadTime":"4220","domReadyTime":"1655","readyStart":"1","redirectTime":"0","appcacheTime":"0","unloadEventTime":"14","lookupDomainTime":"0","connectTime":"0","requestTime":"878","initDomTreeTime":"1008","loadEventTime":"2","navigationStart":"1426401215594","unloadEventStart":"1426401217150","unloadEventEnd":"1426401217164","redirectStart":"1426401215595","redirectEnd":"1426401215595","fetchStart":"1426401215595","domainLookupStart":"1426401215595","domainLookupEnd":"1426401215595","connectStart":"1426401215595","connectEnd":"1426401215595","requestStart":"1426401216271","responseStart":"1426401216041","responseEnd":"1426401217149","domLoading":"1426401217150","domInteractive":"1426401218157","domContentLoadedEventStart":"1426401218158","domContentLoadedEventEnd":"1426401218197","domComplete":"1426401219812","loadEventStart":"1426401219812","loadEventEnd":"1426401219814"},"url":"/events"}user=wadus company=chaflan
```
But you may think that logging more requests events can modify our existing backend metrics, that’s why the engine allows to disable the NewRelic tracking of the requests that it process. Besides our logstash configuration doesn’t count those requests as normal request instead it considers them as stats requests and populate other unrelated librato metrics.
The Real User Measurement doesn’t end on page load events we extended the behaviour to track AJAX requests timings from the user point of view, we track PJAX and turbolinks also. For these kind of interactions we store the referer of the interaction, how much time the interaction spent and the url where it makes the request and we namespace that information by kind of interaction so configuring logstash properly we can modify only the related metrics of each interaction. To properly measure the time that interaction spent we use the User Timing API through the following polyfill usertiming.js. For example:
functiontrackInteraction(type,interactionData)vardata={},url='/_stats',// where you mounted the enginerequest=newXMLHttpRequest();// {pjax: {url: '/users?page=2', referer: '/users', ms: 320}}data[type]=interactionData;request.open('POST',url,true);request.setRequestHeader('Content-Type','application/x-www-form-urlencoded; charset=UTF-8');request.send(data);},false);
Think that you like to measure in average how much time spent your turbolink enhanced page as the user experimenced that interaction or when paginate through AJAX a list of one of your business models, with this engine and our existing infrastructure we answer this question easily and as we already used librato our retention is much more bigger than we need to check how we improve or decline the apps performance.
So to start to improving your application you first need to measure what you want to improve as you see we already did it.
You’ve just read about Real User Measurements on Development, sports, and more by @PacoGuzman.
If you’d prefer to receive your updates in tweet form, please follow me on Twitter, otherwise I hope you’re enjoying the feed!
]]>Before the change the build spent around 10 minutes to complete, we parallelize the build process on 8 jobs than can be run in parallel, we execute 4 jobs per server. So the total time is the one from the less performance job.
But the time running the specs is only around 8:30 so we start reducing the number of jobs that conform the build to 6 jobs only increasing the total time on 1 minute (doing some sums and subtractions) and leaving 2 jobs for other pushes of this application or to run other applications’ build.
Later to detect what were the less performance specs we activated the profile capibilities on our rspec configuration
# spec/spec_helper.rbRSpec.configuredo|config|config.profile_examples=20ifENV['CI']end
With that we detected that the specs that need sphinx to pass were spending more time than expected, so we change some specs that didn’t need that dependency to be there and for the rest we decide to always start sphinx for the suite when running on our CI server. Reviewing the thinking sphinx code there are some sleep statements when starting and stoping the sphinx daemon that introduce a penalty when running the whole suite.
# spec/support/sphinx.rbRSpec.configuredo|config|# On CI we start sphinx only onceifENV['CI']config.before(:suite){ThinkingSphinx::Test.start}config.after(:suite){ThinkingSphinx::Test.stop}elseconfig.before{ThinkingSphinx::Test.startifexample.metadata[:sphinx]}config.after{ThinkingSphinx::Test.stopifexample.metadata[:sphinx]}endend
Later we checked that some specs that ensure the correct behaviour of some custom paperclip extensions spent a lot of time because we test a lot of combinations. The time spent in these specs are mostly related with the commands paperclip execute to generate the thumbnails like the convert and identify command executed through the cocaine gem. To avoid that commands, because we’re not testing those we decided to swap the cocaine command line class to use a custom class that don’t do anything against the file system, for this we used the stub_const
# spec/support/paperclip.rbrequire'fileutils'moduleLightCocaineclassCommandLine<Cocaine::CommandLine# See Paperclip::Thumbnail to now the parameters send to cocainedefrunifcommand=~/^convert/FileUtils.cp(@options[:source].gsub(/\[0\]\Z/,''),@options[:dest])elsifcommand=~/^identify/"400x400"elsesuperendendendendRSpec.configuredo|config|config.beforedoifexample.metadata[:stub_cocaine]stub_const'Cocaine::CommandLine',LightCocaine::CommandLineendendend
So finally we run our test suite in aroung 7:30 minutes so we improved around a 25% of time (10 to 7.5 minutes) and 25% of server resources (8 to 6 jobs).
You’ve just read about How we speed up our test suite on Development, sports, and more by @PacoGuzman.
If you’d prefer to receive your updates in tweet form, please follow me on Twitter, otherwise I hope you’re enjoying the feed!
]]>ElasticSearch es un motor de búsqueda construido sobre Lucene, es Open Source (Apache 2), es distruido y presenta un interfaz Restful. ElasticSearch cuenta con una funcionalidad denominada River. River es un sistema de plugins que una vez instalado en ElasticSearch permite extraer datos (o enviarle datos) que será indexados e incoporados al cluster de ElasticSearch, para su posterior consulta. ElasticSearch cuenta con 4 diferentes rivers listos para ser instalados: Couchdb, RabbitMQ, Twitter y Wikipedia. Aquí nos centraremos en el uso del river para Couchdb.
El river para Couchdb se basa en una funcionalidad de Couchdb denominada Continuous Changes API. Una vez te conectas a este API y manteniendo la conexión abierta Couchdb te enviará las modificaciones y seguirá manteniendo la conexión abierta para enviarte las siguientes modificaciones hasta que decidas cerrar la conexión. Siempre tendremos una conexión abierta (por river definido) pero Couchdb es capaz de manejar un gran número de conexiones sin problemas.
Como instalamos ElasticSearch y el river de Couchdb:
El propio river de Couchdb con la configuración que le hemos dado se enganchará a la siguiente url, para ir recibiendo las modificaciones que hagamos en la base de datos de couchdb e ir indexando documentos.
http://localhost:5984/couchdb_myapp_development/_changes?feed=continuous&include_docs=true&heartbeat=10000
Sin embargo con esta configuración incluimos en el mismo índice todos los documentos de Couchdb, si queremos establecer un indice para dos tipos de documentos como por ejemplo Product y Person, debemos establecer un filtro a la hora de crear el river. Este filtro debe hacer referencia al nombre de un filtro especificado a la hora de definir un design document.
Debemos tener en cuenta que nuestros documentos de couchdb tienen especificado un atributo ‘type’ que permite realizar el filtrado.
Por último crearemos un documento de tipo ‘Product’ y realizaremos una búsqueda
You’ve just read about ElasticSearch y Couchdb river on Development, sports, and more by @PacoGuzman.
If you’d prefer to receive your updates in tweet form, please follow me on Twitter, otherwise I hope you’re enjoying the feed!
]]>Queremos utilizar un servicio para acortar las urls de nuestra aplicación a la hora de compartir dichas urls en redes sociales.
Vamos a hacerlo directamente con JavaScript (lo podríamos hacer en servidor a la hora de definir la url del recurso, pero no es el caso). Tenemos soluciones con jQuery y MootTools. Pero y con Prototype (“ese gran abandonado”).
Prototype no tiene soporte para hacer peticiones ajax a otros dominios así que utilizaremos Ajax.JSONRequest para poder hacer este tipo de peticiones (con razón se abandona :P).
Así que simplemente nos montamos una petición con los parámetros oportunos al api de bit.ly y a aquí va el ejemplo
Ahh se me olvidaba es viernes :P y jsfiddle.net lo peta!
You’ve just read about bit.ly y prototype acortando urls on Development, sports, and more by @PacoGuzman.
If you’d prefer to receive your updates in tweet form, please follow me on Twitter, otherwise I hope you’re enjoying the feed!
]]>Partimos del siguiente controlador que simplemente crea un objecto ActiveRecord::Relation (nuestra aplicación no tiene muchos usuarios no nos preocupamos por recuperar todos los registros)
classAdmin::UsersController<Admin::AdminControllerdefindex@users=User.scopedendend
Ahora simplemente debemos declarar que nuestro controlador y/o acción responde a peticiones csv
classAdmin::UsersController<Admin:AdminControllerrespond_to:html,:csvdefindex@users=User.scopedrespond_with(@users)endend
Sin embago si abrimos nuestro navegador y vamos a “/admin/users.csv” nos encontramos con:
Template is missing Missing template admin/users/index with {:handlers=>[:erb, :rjs, :builder, :rhtml, :rxml], :formats=>[:csv], :locale=>[:es, :es]}
Al utilizar el respond_with y gracias al respond_to :csv hemos indicado a la aplicación que responde a peticiones de formato :csv, pero rails primero comprobará si hemos añadido un bloque para dicho formato o si tenemos una plantilla para hacer el render (index.csv.erb), sino ejecutará un responder por defecto. Y este responder por defecto producirá el error mostrado anteriormente ya que intenta ejecutar igualmente render(:csv => @users) pero no tenemos ninguna plantilla definida.
Pero en rails 3 podemos definir que nuestra aplicación responda a xml y json y funcionariá sin definir ninguna plantilla. Y esto es porque rails 3 define “renderers” tanto para xml y json
classAdmin::UsersController<Admin:AdminControllerrespond_to:html,:csv,:json,:xmldefindex@users=User.scopedrespond_with(@users)endendmoduleActionControllermoduleRenderers...add:jsondo|json,options|json=json.to_json(options)unlessjson.respond_to?(:to_str)json="#{options[:callback]}(#{json})"unlessoptions[:callback].blank?self.content_type||=Mime::JSONself.response_body=jsonendadd:xmldo|xml,options|self.content_type||=Mime::XMLself.response_body=xml.respond_to?(:to_xml)?xml.to_xml(options):xmlend...endend
Entonces simplemente lo que necesitamos es añadir un nuevo “renderer” para el formato csv. Nos creamos el initializer csv_renderer.rb
ActiveSupport.on_load:action_controllerdoActionController.add_renderer:csvdo|relation,options|csv_instance=CsvDelegator.new(relation)options={:type=>"text/csv; charset=utf8",:filename=>csv_instance.filename}.merge(options)send_datacsv_instance.to_csv,options.slice(:type,:filename)endend
Y no necesitamos añadir nada más, bueno si definir nuestra clase CsvDelegator, pero eso no tiene mucha historia y os lo dejo a vosotros
Y bueno unos links que pueden resultar de interes: - Rails 3 upgrade - Render Options in Rails 3 - Creating your own renderer - Three reasons love responder
NOTA: ¿Por qué en los diferentes ejemplos utilizan ActionController::Renderers.add en lugar de ActionController.add_renderer? ¡Qué alguien me cuente!
You’ve just read about Sencillo renderer de CSV en Rails 3 on Development, sports, and more by @PacoGuzman.
If you’d prefer to receive your updates in tweet form, please follow me on Twitter, otherwise I hope you’re enjoying the feed!
]]>En primer lugar después de incluir fastercsv en mi Gemfile y tratar de arrancar la aplicación me encuentro con el siguiente mensaje:
Please switch to Ruby 1.9’s standard CSV library. It’s FasterCSV plus support for Ruby 1.9’s m17n encoding engine.
Bueno parece que no necesito ningúna libreria extra solo utilizar la librería standard de CSV de Ruby 1.9. Pues nada me creo un initializer dependencies.rb e incluyo un require ‘csv’. Ahora manos a la obra con el CSV en ruby 1.9, empezamos probando cosas, en un irb:
CSV.generate{|csv|csv<<["nombre","apellidos"]}#=> "nombre,apellidos\n"
Bien pinta bien, ahora unos carácteres especiales (unas tildes por ejemplo)
CSV.generate{|csv|csv<<["dirección","población"]}#=> "direcci\xC3\xB3n,poblaci\xC3\xB3n\n"
Vale ya aparecen los primeros problemas con la codificación. Yo siempre he trabajo en “UTF-8” tanto a nivel de aplicación como en base de datos, así que supongo que deberé especificar la codificación en algún sitio. Esto en ruby 1.9 es fácil, e igual de fácil pasarselo a la libreria de CSV.
CSV.generate(:encoding=>"".encoding){|csv|csv<<["dirección","población"]}#=> "dirección,población\n"
La cosa mejora y me creo un fichero dentro del directorio lib de mi aplicación rails 3
classCsvBlogdefself.to_csvCSV.generate(:encoding=>"".encoding){|csv|csv<<["dirección","población"]}endend
Ahora en lugar de arrancar el irb arranco la consola de rails:
CsvBlog.to_csv/lib/csv_blog.rb:5:invalidmultibytechar(US-ASCII)/lib/csv_blog.rb:5:syntaxerror,unexpected$end,expecting']'csv<<["dirección","población"]
Bueno parece que rails 3 no establece la codificación para los ficheros dentro del directorio lib de la aplicación, así que toca especificar la codificación en este nuevo fichero y ya no tenemos mayor problema. Pero queda feo tener que especificar la codificación al generar el csv, pero miremos lo que ocurre:
CSV.generate{|csv|puts"".encoding;csv<<["dirección"]}# UTF-8 => "direcci\xC3\xB3n\n"CSV.generate{|csv|putscsv.encoding;csv<<["dirección"]}# US-ASCII => "direcci\xC3\xB3n\n"CSV.generate(""){|csv|putscsv.encoding;csv<<["dirección"]}# UTF-8 => "dirección\n"
<La codificación de nuestros datos es UTF-8 pero si no se especifica a la librería mediante la opción :encoding o mediate un string (extrae la codificación de el), el objeto csv nos hace un estropicio.
Hasta aquí todo en la próxima entrega comentaré como integrar esto con los Responders o Renderers incluidos en rails 3, el tema nos va a quedar muy sencillo.
You’ve just read about CSV y ruby 1.9 y el 'encoding' (yo lo escribo 'enconding') on Development, sports, and more by @PacoGuzman.
If you’d prefer to receive your updates in tweet form, please follow me on Twitter, otherwise I hope you’re enjoying the feed!
]]>En ASPgems hemos estado organizando charlas de formación interna una vez al mes, abiertas a cualquiera que se quisiera apuntar, para contar/exponer materias de interés para nosotros. Y con estas charlas llego mi estreno, con una charla sobre testing de aplicaciones ruby/rails junto a mi compañero @jorgegorka, incluso hay un vídeo, en el canal de aspgems en vimeo. No fue nada espectular, no se podía esperar otra cosa si yo andaba ahí metido, después sobre todo de querer mostrar código en directo y abandonarlo a los 10 minutos :) También hemos realizado alguna charla más informal al estilo “lightning talks” muy insperadoras de lo que esta por llegar…en la web
También durante este tiempo he estado revisando código de aquí y de allá, he liberado pequeños pedacitos de código en forma de plugins para aplicaciones tog: tog_activity y tog_wall para compensar todo el curro que se ha dado la gente de tog
He seguido apoyando mundo-pepino sobre todo después de una charla del madrid-rb sobre capybara de @porras que me empujo a actualizar mundo-pepino para que se pudieran utilizar las últimas versiones de cucumber y para que se pudiera utilizar capybara o webrat, y de aquí surgió la Paco’s release</a> gracias Nando</a>! ;)
También participe en el Desafio Abredatos junto con: @eLafo, @jorgegorka@leptom y bueno la verdad que lo pasamos genial creando DesenchufaTuCasa, viendo la formula 1 el domingo, zampandonos unas pizzas del dominos, vamos zampando toda la comida rápida que pudimos.
Y bueno también he tenido tiempo de participar en el pasado BugMash, he entrado en la Rails Contributors, he estado en Amsterdam una semanita de vacaciones.
Y lo que viene seguro que es mejor, empezando la semana que viene con el viaje a la Euruko 2010
You’ve just read about Mis últimos meses con RoR on Development, sports, and more by @PacoGuzman.
If you’d prefer to receive your updates in tweet form, please follow me on Twitter, otherwise I hope you’re enjoying the feed!
]]>Además error_messages_for tiene multitud de opciones que nos permiten definir enteramente el contenido de los mensajes, su estructura html y otras cosillas. Pero el problema viene cuando tratamos con los valores por defecto. Este método captura la variable de instancia a partir de su primer parámetro y debe obtener la variable options[object_name] de dicha variable si la opción object_name no es pasada como parámetro. Y aquí llegamos a la controversia, ¿Que clave de nuestros locale recuperamos para generar el object_name en caso de que no lo proporcione el programador?
Pues como suponéis error_messages_for no recupera la misma clave que human_name con lo que nos surge un problema.
deferror_messages_for(*params)options[:object_name]||=params.first...I18n.with_options:locale=>options[:locale],:scope=>[:activerecord,:errors,:template]do|locale|...object_name=options[:object_name].to_s.gsub('_',' ')object_name=I18n.t(object_name,:default=>object_name,:scope=>[:activerecord,:models],:count=>1)...end...enddefhuman_name(options={})defaults=self_and_descendants_from_active_record.mapdo|klass|:"#{klass.name.underscore}"enddefaults<<self.name.humanizeI18n.translate(defaults.shift,{:scope=>[:activerecord,:models],:count=>1,:default=>defaults}.merge(options))end
Pero ante la llegada inminente de Rails 3 o eso nos comentaba Yehuda en el grupo del core team se han puesto de acuerdo estos muchachos con la ayuda de ActiveModel y tenemos esto.
moduleActiveModel::Naming# Transform the model name into a more humane format, using I18n. By default,# it will underscore then humanize the class name (BlogPost.model_name.human #=> "Blog post").# Specify +options+ with additional translating options.defhuman(options={})# No nos interesa que clave recupera pero vemos que será la misma...endendmoduleActionViewmoduleHelpersmoduleActiveModeldeferror_messages_for(*params)...ifobject.class.respond_to?(:model_name)options[:object_name]||=object.class.model_name.human.downcaseend...endendendend
Así por ahora solo nos queda esperar o pasarle a error_messages_for el parámetro object_name con le valor que necesitemos.
Ciau
You’ve just read about human_name and error_message_for controversy on Development, sports, and more by @PacoGuzman.
If you’d prefer to receive your updates in tweet form, please follow me on Twitter, otherwise I hope you’re enjoying the feed!
]]>Application developers should not use this module directly.
Pero yo no estoy totalmente de acuerdo con esto y suelo utilizar la funcionalidad que ofrece este módulo a mi antojo. Por un lado no me gusta meter includes al definir las asociaciones en los modelos, sino hacerlo al recuperar los registros en los controladores y añadir los includes que necesite para las vistas que vaya a mostrar.
También suele ocurrir que el número de includes a utilizar es grande por lo que una vez recuperados los registros en el controlador (paginados o no) realizo la carga de las asociaciones a posteriori. (¿Por qué haberlo hecho en el modelo?)
Vale hasta aquí tal vez no haya sido nada convincente y seguro que mis razones sean puramente estéticas sin entrar en detalles de rendimiento. Aún así me he encontrado con un caso, tal vez un “patrón”, en el que la precarga de asociaciones puede reducir el número de query’s a realizar en la base de datos. Siempre hablando del eager loading que realiza Rails desde la versión 2.1 y cuando no se introducen condiciones en los registros asociados.
El caso consta de los modelos comment, user y profile descritos aquí
Básicamente un perfil de usuario puede ser comentado a través de un comentario root “root_graffity”. Este comentario root puede recibir comentarios “sons” y cada uno de esos comentarios acepta respuestas “replys”. En el caso de querer recuperar lo que he denominado “graffities” junto con todos sus datos debería hacer lo que muestro aquí
Como vemos se realizan 7 consultas a la base de datos, y el detalle está en que a la tabla de users y profiles se accede en dos ocasiones. Esto último lo podemos evitar si realizar un precarga de asociaciones tal y como muestro aquí
Si estamos cargando asociaciones auto-referenciadas (o algo así), es decir, aquellas que cargan modelos de la misma clase, si esta clase necesita asociaciones de otras clases. Veo que puede ser de utilidad agrupar los modelos de la primera clase y para ese conjunto cargar sus asociaciones. Ya que lo que hace Rails en el módulo en cuestión es recuperar los registros asociados a partir de las claves que contiene la colección de registros cuyas asociaciones se quieren precargar.
Alé que es sábado y habrá que beberse unas cervezas! A vuestra salud!
You’ve just read about Precarga de Asociaciones - Usandolo a mi antojo on Development, sports, and more by @PacoGuzman.
If you’d prefer to receive your updates in tweet form, please follow me on Twitter, otherwise I hope you’re enjoying the feed!
]]>