<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pete Keen</title>
    <description>The latest articles on DEV Community by Pete Keen (@zrail).</description>
    <link>https://dev.to/zrail</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F95078%2Fb4a8d391-84d0-41e9-a9fb-1ffd15c5ba50.jpeg</url>
      <title>DEV Community: Pete Keen</title>
      <link>https://dev.to/zrail</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zrail"/>
    <language>en</language>
    <item>
      <title>Using Que instead of Sidekiq</title>
      <dc:creator>Pete Keen</dc:creator>
      <pubDate>Wed, 13 Mar 2019 18:38:14 +0000</pubDate>
      <link>https://dev.to/zrail/using-que-instead-of-sidekiq-19gi</link>
      <guid>https://dev.to/zrail/using-que-instead-of-sidekiq-19gi</guid>
      <description>&lt;p&gt;A project I've had on the back burner for quite awhile is my own little marketing automation tool.&lt;br&gt;
Not that existing tools like Drip or ConvertKit aren't adequate, of course.&lt;br&gt;
They do the job and do it well.&lt;/p&gt;

&lt;p&gt;I enjoy owning my own infrastructure, however, and after Drip changed direction and raised prices I found myself without a home for my mailing list.&lt;br&gt;
I thought, why not now?&lt;/p&gt;

&lt;p&gt;One vital component of any broadcast email system is &lt;strong&gt;fanout&lt;/strong&gt;, where you merge the message you want to send with the list of people that should receive it.&lt;br&gt;
The easiest way to fanout is to just loop over the list of recipients and enqueue a job for each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;not_opted_out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="no"&gt;BroadcastMessageDeliver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;the_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This is simple and works great. However, it's not super efficient. We can do better.&lt;/p&gt;

&lt;p&gt;If we're using Sidekiq we can use &lt;code&gt;push_bulk&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;not_opted_out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_in_batches&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_bulk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s1"&gt;'BroadcastMessageDeliver'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="ss"&gt;args: &lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;the_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;find_in_batches&lt;/code&gt; call is a built-in ActiveRecord method that will give you all of the records in the scope in batches, which is just an array of ActiveRecord objects.&lt;br&gt;
&lt;code&gt;Sidekiq::Client.push_bulk&lt;/code&gt; eliminates the vast majority of Redis round trips that the naive version does because it pushes the whole batch in one Redis call.&lt;/p&gt;

&lt;p&gt;We can still do better, though. Instead of using Sidekiq we can use &lt;a href="https://github.com/chanks/que"&gt;Que&lt;/a&gt;.&lt;br&gt;
Que is a background processing system like Sidekiq that keeps jobs in a PostgreSQL table instead of in a Redis list.&lt;br&gt;
It uses PostgreSQL's native &lt;code&gt;listen/notify&lt;/code&gt; system to make job starts basically instantenous, rather than polling like what &lt;code&gt;DelayedJob&lt;/code&gt; does.&lt;/p&gt;

&lt;p&gt;Using the database as the queue has a number of advantages over systems that use two data stores. In particular, ACID guarantees and atomic backups are important to me because I'm running this all myself. The fewer moving parts the better.&lt;/p&gt;

&lt;p&gt;The other thing you can do is &lt;strong&gt;insert directly into the &lt;code&gt;que_jobs&lt;/code&gt; table&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sx"&gt;%Q{
  INSERT INTO que_jobs (job_class, args)
  SELECT
    'BroadcastMessageDeliver' as job_class,
    jsonb_build_array(&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;the_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sx"&gt;, x.id) as args
  FROM
    (&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;not_opted_out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_sql&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sx"&gt;) x
}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;que_jobs&lt;/code&gt; table is just a database table, which means you can insert into it however you want.&lt;br&gt;
For example, &lt;code&gt;Que::Job.enqueue&lt;/code&gt; just creates a record and saves it, it doesn't use any ActiveRecord hooks at all.&lt;/p&gt;

&lt;p&gt;We can eliminate almost every round trip and application-level loop by letting the database do all the work.&lt;/p&gt;

&lt;p&gt;Benchmarks (local Redis and local PostgreSQL, 5000 records):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sidekiq loop: 1.9 seconds&lt;/li&gt;
&lt;li&gt;Sidekiq batches: 0.3 seconds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Que direct insert: 0.7 seconds&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wait... that's... slower?&lt;/p&gt;

&lt;p&gt;I'm as surprised as you are, but there turns out to be a pretty good reason.&lt;br&gt;
Que performs a bunch of check constrants on the incoming data to make sure it's coherent and ready to run. Here's all the things it checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check constraints:
    "error_length"
    "job_class_length"
    "queue_length"
    "valid_args"
    "valid_data"
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;code&gt;valid_data&lt;/code&gt; in particular does a handful of expensive-ish operations on the incoming &lt;code&gt;json&lt;/code&gt; data.&lt;/p&gt;

&lt;p&gt;So I guess the lesson here is to always validate your assumptions.&lt;br&gt;
I assumed that eliminating round trips would make things faster but because of other constraints and validations it's actually slower.&lt;/p&gt;

&lt;p&gt;Still, it's considerably faster than the naive version (which is still no slouch, let's be honest), my marketing system gets all those in-database queue benefits, and I find it aesthetically pleasing. I think I'll keep it.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>My Own Private CDN</title>
      <dc:creator>Pete Keen</dc:creator>
      <pubDate>Thu, 30 Aug 2018 19:53:43 +0000</pubDate>
      <link>https://dev.to/zrail/my-own-private-cdn-2e1i</link>
      <guid>https://dev.to/zrail/my-own-private-cdn-2e1i</guid>
      <description>&lt;p&gt;Hosting my own CDN has long been a completely irrational goal of mine. &lt;em&gt;Wouldn't it be neat,&lt;/em&gt; I'd think, &lt;em&gt;if I could tweak every knob instead of relying on CloudFront to do the right thing?&lt;/em&gt; Recently I read &lt;a href="https://pasztor.at/blog/building-your-own-cdn"&gt;this article&lt;/a&gt; by Janos Pasztor about how he built a tiny CDN for his website. This just proves to me that at least it's not an &lt;em&gt;uncommon&lt;/em&gt; irrational thought.&lt;/p&gt;

&lt;p&gt;Yesterday I decided to actually start building something. Even if it doesn't make it into production, I'll at least have learned something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Goals
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Centrally manage all of the dozen or so sites that I run&lt;/li&gt;
&lt;li&gt;Automatically generate and renew LetsEncrypt certificates, both for publicly-facing sites and my own private sites. This means using the dns-01 challenge instead of using the easier to understand http challenge.&lt;/li&gt;
&lt;li&gt;Easily add new cache nodes with authenticated &lt;code&gt;curl | sudo bash&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Automatically reconfigure &lt;code&gt;nginx&lt;/code&gt; on the cache nodes when certificates roll or sites change&lt;/li&gt;
&lt;li&gt;Easily host sites anywhere, including the internet-inaccessible server in my basement&lt;/li&gt;
&lt;li&gt;Stop paying so much for bandwidth. Transfer is $5/tb/mo from DigitalOcean vs $$$$ for CloudFront.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, I really want to learn how LetsEncrypt works. &lt;code&gt;certbot&lt;/code&gt; is great but it is very much a black box to me. Command-line arguments in, certificates out.&lt;br&gt;
If I write my own management system I can actually learn how the guts work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current Status
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;basic Rails app that knows about sites and proxies&lt;/li&gt;
&lt;li&gt;creating or updating a site (re)generates a LetsEncrypt certificate for all of the domains that point at that site&lt;/li&gt;
&lt;li&gt;wildcard domains are fully supported&lt;/li&gt;
&lt;li&gt;authenticated endpoint that generates a zip file of all of the certificates and private keys&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Automatic certificate refresh using something like &lt;a href="https://github.com/ondrejbartas/sidekiq-cron"&gt;Sidekiq Cron&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Deploy onto the server in my basement on my &lt;a href="https://zerotier.com"&gt;ZeroTier network&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Move all of my existing LetsEncrypt &lt;code&gt;certbot&lt;/code&gt; crons into this system&lt;/li&gt;
&lt;li&gt;Provision a POP by hand and then automate the steps to provision another one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd like to follow along I put the project up on &lt;a href="https://github.com/peterkeen/diycdn"&gt;GitHub&lt;/a&gt;. I'll also be posting updates here as I go.&lt;/p&gt;




&lt;p&gt;Want more stuff like this? &lt;a href="https://www.petekeen.net/newsletter"&gt;Sign up for my mailing list&lt;/a&gt;. I post everything there two weeks before I post it here.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ruby</category>
      <category>rails</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
