<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Jason Taylor - Developer Extraordinaire</title>
    <description>This a fancy blog that is for fancy code.
</description>
    <link>https://thetizzo.com/</link>
    <atom:link href="https://thetizzo.com/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Fri, 12 Dec 2025 03:46:19 +0000</pubDate>
    <lastBuildDate>Fri, 12 Dec 2025 03:46:19 +0000</lastBuildDate>
    <generator>Jekyll v4.4.1</generator>
    
      <item>
        <title>Scaling Elasticsearch for Fun and Profit</title>
        <description>&lt;p&gt;I have been managing an Elasticsearch cluster at work for over 3 years. This time has been a constant learning experience to say the least. When I started working on this cluster, we were using a cloud provider that gave us exactly one tool for scaling up: a slider to provision more nodes and larger nodes. The standard practice at the time was to slide the slider up whenever the cluster started acting up&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. This would give us “more capacity” for a couple months and then the acting up process would start all over again. This method of scaling also had the notable side effect of generating massive bills.&lt;/p&gt;

&lt;p&gt;This post is a list of everything I tried that gave me positive results when the cluster was handling load or a concept that was important in helping me understand more about how Elasticsearch runs. The list is in no particular order, but I tried to group it by topic to make it easier to read. The most important takeaway you can have from this post is to make sure you are monitoring enough to see what effect each change has on performance as you apply it. This will help you determine what the cluster is struggling with the most and help you prioritize what to try next.&lt;/p&gt;

&lt;p&gt;At the time of my applying the techniques described below we were using Elasticsearch 6, but I think a lot of this advice is applicable on Elasticsearch 7 as well. After version 7.10, Elastic changed licenses for Elasticsearch and it is no longer open source, so buyer beware. :)&lt;/p&gt;

&lt;h3 id=&quot;general-advice&quot;&gt;General Advice&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Start with 3 large generic nodes and go as far as you can with this.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;Specialized nodes increase complexity and cost quite a bit but don’t necessarily speed up writes or searches. Best to stay on the simplest possible cluster configuration and focus on optimizing your indexes and data.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Pay someone else to run your cluster.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;We have a vendor who runs the actual hardware for our cluster. They manage the nodes and provisioning; we manage the data in the cluster. I highly recommend this if you have the budget, running a cluster yourself would 10x the difficulty of managing Elasticsearch in your system unless you have someone on your team with this specific expertise. It’s worth it to pay someone else for this.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Learn the terminology.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;Spend the time reading the &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/6.8/elasticsearch-intro.html&quot;&gt;docs&lt;/a&gt; to learn the terminology specific to Elasticsearch because this will make life much easier when you need to find answers on the Internet. Not to mention you will pick up a ton of incidental knowledge about how Elasticsearch works which makes working on it a lot easier.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Be aware of the default settings of the &lt;code&gt;elasticsearch-transport&lt;/code&gt; library.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;For example, &lt;a href=&quot;https://github.com/elastic/elasticsearch-ruby/tree/7.16/elasticsearch-transport#retrying-on-failures&quot;&gt;the Ruby version retries a request up to 3 times by default&lt;/a&gt; if it fails on a connection issue or a timeout. For us, this led to compounding issues on the cluster whenever we had enough load that the cluster started to reject some requests because they would all immediately retry 3 times instead of going through our exponential backoff retry cycle (more on this in a later section).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Monitor the Write and Search queue size and rejections.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;This is the best &lt;a href=&quot;https://en.wikipedia.org/wiki/Sentinel_species&quot;&gt;“canary in the coal mine”&lt;/a&gt; indicator for when your cluster is starting to struggle. Each node in your cluster has as Write and Search queue. Incoming requests are automatically queued into these depending on whether they are a write requests or a search requests and then handled in order.&lt;/p&gt;

    &lt;p&gt;If these queues fill up the cluster will immediately start rejecting requests with an error. When this happens, you will see a spike in the rejections stats that the cluster emits in the metrics. A few of these here and there are normal and should get handled by your retry logic, however if these start to add up, you need intervention or you’re going to have a bad time.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Don’t put Elasticsearch in the hot path.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;In my experience, having Elasticsearch in the hot path does not go well as soon as you scale to the point of having a decent amount of load on the server. If your site requires Elasticsearch to be instantly responsive to a request to load a main page, you’re going to have a bad time.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;index-management&quot;&gt;Index Management&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Use aliases.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;Without aliases, managing your indexes will become very difficult without requiring scheduled downtime for your cluster. Aliases give you a way to perform a lot of index maintenance tasks, such as re-indexing, while always having a live index in use by users.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Some settings can only be changed on index creation.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;Be aware of certain settings that can only be changed at index creation time like &lt;code&gt;number_of_shards&lt;/code&gt; for example. Changing these after the index is created requires creating a whole new index and migrating the data to it. If you have large indexes, this can take several hours. Switching indexes is when your aliases will come in handy.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Avoid using anything other than default index settings.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;The defaults will take you a long way and by the time you might need to fiddle with a lot of these, you will have learned enough to know what they do and how they might affect the system. After 4 years of running this cluster, the only setting I have needed to change on an index is &lt;code&gt;number_of_shards&lt;/code&gt;.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Indexes that have different workloads on the same cluster is difficult.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;Let’s say you have an index that is a mostly static dataset but gets searched all the time, and you have a second index that receives tons of writes but doesn’t get searched that much. It can be difficult to tune the cluster well for opposite workloads like this. The reason for this is that you essentially have one shared pool of resources on each node, so if you optimize too much for writes, then search performance will become resource constrained and begin to suffer and vice versa. It’s good to be aware of this and always maintain a good balance when tuning.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Primary shard vs Replica shards.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;You should set &lt;code&gt;number_of_shards&lt;/code&gt; (aka primary shards) to the number of data nodes in your cluster and &lt;code&gt;number_of_replicas&lt;/code&gt; to &lt;code&gt;1&lt;/code&gt; to ensure that the index has an even distribution of shards across the cluster. This will result in the index having a primary shard and a replica shard on each node in the cluster which is ideal. For larger indexes you can set &lt;code&gt;number_of_shards&lt;/code&gt; higher but it should always be a multiple of the number of data nodes in the cluster to ensure that you have even distribution. Having an uneven distribution of shards can unbalance the load between nodes and lead to some nodes running hot while others are under-utilized.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;~50GB per shard is a good rule of thumb.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;Aim for ~50GB per shard as a maximum shard size. Shards that are larger than this will become slow to search and write to and other operational tasks the cluster performs in the background (i.e. merges) will become less performant. If your index starts to exceed 50GB per shard, you may want to increase the &lt;code&gt;number_of_shards&lt;/code&gt; on that index.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;handling-writes-gracefully&quot;&gt;Handling Writes Gracefully&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Do not make write requests inline with a web request.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;Elasticsearch by its nature is slow at writes compared to a relational database like Postgres. This is because to write a new object, Elasticsearch isn’t just inserting the data in the index, it’s also processing it by whatever rules you have set up to index it for searching.  This takes a lot of time, especially if you have more complex search requirements.  The best solution for this is to farm this out to an asynchronous process, like a job queue or data stream for processing later.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Implement exponential backoff for retries.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;Under conditions where the cluster is having trouble keeping up with the amount of load, you definitely don’t want your jobs to keep slamming it with the same write request over and over. If it tells you it needs help, give it some help in the form of backing off the retries exponentially. Basically instead of retrying a failed job every second, you would instead retry after 1 second, 2 seconds, 4 seconds, 16 seconds, etc.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Use the Bulk API and actually do writes in batches.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;The &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/6.8/docs-bulk.html&quot;&gt;Bulk API&lt;/a&gt; was added by Elasticsearch to handle indexing and deleting operations in bulk. This is much faster than sending requests through the Index or Delete APIs for individual documents. Every blog post you can find will tell you this.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Build indexes with a document structure that requires as few updates as possible.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;When Elasticsearch updates a document in the index with new values, it doesn’t update the existing document in place. Instead, it creates a new document, indexes that document from scratch, then deletes the old document. Not only is this slow, but it also leads to bloated indexes because the deleted documents do not get removed from the index, they just get marked as deleted. I have seen indexes with more than 100GB of bloat due to this issue, to the point where we have to periodically run manual re-indexing operations on our larger indexes to keep them from becoming too bloated. This takes several hours to run on a large index.&lt;/p&gt;

    &lt;p&gt;A better path forward here is to construct your index in a way where you can basically append new data to it if possible. For example, if we have an index of containing documents that represent text message conversations that might look something like this:&lt;/p&gt;
    &lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;_source&quot;: {
    &quot;id&quot;: &amp;lt;conversation_id&amp;gt;,
    &quot;messages&quot;: [
      { &quot;id&quot;: &amp;lt;message_id&amp;gt;, &quot;body&quot;: &quot;message 1&quot; },
      { &quot;id&quot;: &amp;lt;message_id&amp;gt;, &quot;body&quot;: &quot;message 2&quot; }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;Each time we receive a new message for a conversation, we update the conversation in the index to add the new message body in the messages list for the conversation. This requires Elasticsearch to reindex the entire conversation document for search even though most of the data is unchanged. This means we’re using cluster resources to index the same data repeatedly, while leaving deleted versions of the conversation laying around and filling up the storage space.&lt;/p&gt;

    &lt;p&gt;However, since we know that once a text message is sent, the body of the message doesn’t change, we could refactor this index to be an index of messages instead of conversations:&lt;/p&gt;
    &lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;_source&quot;: {
    &quot;id&quot;: &amp;lt;message_id&amp;gt;,
    &quot;conversation_id&quot;: &amp;lt;conversation_id&amp;gt;,
    &quot;body&quot;: &quot;message 1&quot;,
  }
},
{
  &quot;_source&quot;: {
    &quot;id&quot;: &amp;lt;message_id&amp;gt;,
    &quot;conversation_id&quot;: &amp;lt;conversation_id&amp;gt;,
    &quot;body&quot;: &quot;message 2&quot;,
  }
}
&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;This enables several benefits that we didn’t have using the old structure.&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;Small, concise documents for quicker indexing on write.&lt;/li&gt;
      &lt;li&gt;No updates, just add new messages as new documents.&lt;/li&gt;
      &lt;li&gt;Ability to use the &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/6.8/indices-rollover-index.html&quot;&gt;Rollover Index API&lt;/a&gt; to manage size of the index.&lt;/li&gt;
      &lt;li&gt;No bloating of deleted documents over time which removes the need to Reindex periodically&lt;/li&gt;
      &lt;li&gt;More CPU resources available for search requests since they aren’t being spent on writing whole conversations repeatedly.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Documents in the index should have a flat structure.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;Documents in the index should have as flat a structure as possible.  Having a deeply nested structure doesn’t perform well because Elasticsearch separately indexes each node in the document as a separate document which takes a lot of processing power on write and leads to large, bloated indexes which need more shards, which then makes search slower.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;monitoring&quot;&gt;Monitoring&lt;/h3&gt;

&lt;p&gt;Here is the list of metrics I’ve found helpful to monitor over the years.&lt;/p&gt;

&lt;h4 id=&quot;es-node-stats&quot;&gt;ES Node Stats&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;Searches and writes&lt;/li&gt;
  &lt;li&gt;Write queue size&lt;/li&gt;
  &lt;li&gt;Write queue rejections&lt;/li&gt;
  &lt;li&gt;Search queue size&lt;/li&gt;
  &lt;li&gt;Search queue rejections&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;es-index-stats&quot;&gt;ES Index Stats&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;Searches and writes&lt;/li&gt;
  &lt;li&gt;Data size&lt;/li&gt;
  &lt;li&gt;Document count&lt;/li&gt;
  &lt;li&gt;Shard count&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;job-stats&quot;&gt;Job Stats&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;Queue lag&lt;/li&gt;
  &lt;li&gt;Time to process&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h3&gt;

&lt;p&gt;So if you’ve made it this far into reading this, good luck with your cluster! Elasticsearch can run quite well if you are thoughtful about how you structure your data and set up your interactions between the application and cluster well. Our cluster has gone from being a consistent source of outages to stable enough that it “Just Runs” and reaching that point feels like real success. It can be done!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Footnotes:&lt;/em&gt;&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;Acting up, in this case, being defined as the cluster becoming unresponsive due to being unable to process the volume of requests it was receiving. This ultimately would cause the site to go down because the web servers would be saturated with requests that were waiting on responses from the cluster. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Tue, 13 Feb 2024 00:00:00 +0000</pubDate>
        <link>https://thetizzo.com/2024/02/13/scaling-elasticsearch-for-fun-and-profit</link>
        <guid isPermaLink="true">https://thetizzo.com/2024/02/13/scaling-elasticsearch-for-fun-and-profit</guid>
        
        <category>howto</category>
        
        
      </item>
    
      <item>
        <title>Building An Online Resume With Jekyll &amp; pdfmake</title>
        <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;UPDATE 2024-01-23:&lt;/em&gt;&lt;/strong&gt; Since writing this I have removed this version of my resume from the site. If you’re still interested in how this was working, you can find all the resume related code in &lt;a href=&quot;https://github.com/thetizzo/thetizzo.github.io/commit/bcda711d0d864595d49add2ece1ca2442ab516f3&quot;&gt;this commit from the repo&lt;/a&gt;. GL;HF!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;ORIGINAL POST:&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One day it dawned on me, I am a web developer but I was still manually maintaining my resume in a Word document.  This was not ideal because it was difficult to keep versions and did little to show off any of my relevant skills in web development. To solve this problem I decided to build an online version of my resume which I could use to show prospective employers that I can, in fact, build things on the Internet.&lt;/p&gt;

&lt;p&gt;My first step was to grab all the existing data from my Word based resume and manually build it into HTML so that I could put it on my site. This worked great but when I had to update my resume there were now two copies of it to maintain, one in Word and one in HTML.&lt;/p&gt;

&lt;p&gt;At this point I took a step back and thought about what my requirements really were. I needed the following:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;One source of data to rule them all&lt;/li&gt;
  &lt;li&gt;Support essential formats (HTML, PDF)&lt;/li&gt;
  &lt;li&gt;Generate PDF automatically so it’s always up to date&lt;/li&gt;
  &lt;li&gt;Generate PDF on client side since my site is on GitHub Pages so it can’t be done on the server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enter &lt;a href=&quot;https://jekyllrb.com/docs/datafiles/&quot;&gt;Jekyll Data Files&lt;/a&gt; and &lt;a href=&quot;http://pdfmake.org/#/&quot;&gt;pdfmake&lt;/a&gt; to save the day!&lt;/p&gt;

&lt;h2 id=&quot;jekyll-data-files&quot;&gt;Jekyll Data Files&lt;/h2&gt;
&lt;p&gt;Built into Jekyll is a super handy way to create your own sets of data and make them available to the site as a whole.&lt;/p&gt;

&lt;p&gt;To do this you simply add a directory called &lt;code&gt;_data&lt;/code&gt; to your Jekyll project, then add a file in this directory with the name of the data set you want to represent.  In my case this file is called &lt;code&gt;resume.yml&lt;/code&gt;.   This can be referenced elsewhere in the project with &lt;code&gt;site.data.resume&lt;/code&gt;. Jekyll supports JSON, CSV, or YAML formats for these data files.  I used YAML because, for me, it’s the most readable.&lt;/p&gt;

&lt;p&gt;At this point it was easy to use this data to generate my resume because I could simply loop through the data to build various sections of my resume.  As an example, here is the code that builds the jobs section:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- _includes/jobs_section.html --&amp;gt;
{% for job in resume.jobs %}
  &amp;lt;div&amp;gt;
    &amp;lt;h3&amp;gt;
      &amp;lt;a href={{ job.company.website }} target=&quot;_blank&quot;&amp;gt;{{ job.company.name }}&amp;lt;/a&amp;gt;
    &amp;lt;/h3&amp;gt;

    {% for position in job.positions %}
      &amp;lt;div&amp;gt;
        &amp;lt;span&amp;gt;{{ position.title }}&amp;lt;/span&amp;gt;
        &amp;lt;span&amp;gt;({{ position.duration }})&amp;lt;/span&amp;gt;
      &amp;lt;/div&amp;gt;
    {% endfor %}

    &amp;lt;p&amp;gt;{{ job.description }}&amp;lt;/p&amp;gt;

    &amp;lt;ul&amp;gt;
      {% for accomplishment in job.accomplishments %}
        &amp;lt;li&amp;gt;{{ accomplishment }}&amp;lt;/li&amp;gt;
      {% endfor %}
    &amp;lt;/ul&amp;gt;
  &amp;lt;/div&amp;gt;
{% endfor %}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point I had a working web page for my resume of but I still needed to make the PDF.&lt;/p&gt;

&lt;h2 id=&quot;pdfmake&quot;&gt;pdfmake&lt;/h2&gt;
&lt;p&gt;Since I run my Jekyll site on GitHub Pages, I am restricted in terms of the amount of server side processing that I can do so I had to find a library that was capable of generating the PDF on the client side. I tried out several JavaScript libraries capable of converting JSON into PDF in the browser.  The winner ended up being &lt;code&gt;pdfmake&lt;/code&gt; because it has good documentation and was the easiest for me to get up and running.&lt;/p&gt;

&lt;p&gt;In order for &lt;code&gt;pdfmake&lt;/code&gt; to do it’s magic though, I needed to get the data for my resume onto the client side in JSON format.  Thankfully this is trivially easy in Jekyll.  I just put it into a hidden div on the same page as the HTML resume and converted the data hash to JSON with Liquid’s very handy &lt;code&gt;jsonify&lt;/code&gt; utility.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- _layouts/resume.html --&amp;gt;
&amp;lt;div hidden id=&quot;resumeJson&quot;&amp;gt;
  {{ site.data.resume | jsonify }}
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I then wrote a simple JS function that processes the JSON and builds document definition object that includes styles and formatting that &lt;code&gt;pdfmake&lt;/code&gt; needs to be able to generate the PDF.  That function can be found &lt;a href=&quot;https://github.com/thetizzo/thetizzo.github.io/blob/master/assets/javascripts/resume.js&quot;&gt;here on GitHub.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that’s pretty much it!&lt;/p&gt;
</description>
        <pubDate>Tue, 02 Jan 2018 00:00:00 +0000</pubDate>
        <link>https://thetizzo.com/2018/01/02/building-an-online-resume-with-jekyll-and-pdfmake</link>
        <guid isPermaLink="true">https://thetizzo.com/2018/01/02/building-an-online-resume-with-jekyll-and-pdfmake</guid>
        
        <category>howto</category>
        
        
      </item>
    
      <item>
        <title>Authentication with Devise &amp;amp; Turbolinks for iOS</title>
        <description>&lt;p&gt;I previously published this article with the first version of how I got &lt;a href=&quot;https://github.com/plataformatec/devise&quot; target=&quot;_blank&quot;&gt;Devise 4.2&lt;/a&gt;
 working with &lt;a href=&quot;https://github.com/turbolinks/turbolinks&quot; target=&quot;_blank&quot;&gt;Turbolinks 5&lt;/a&gt;
 through an iOS application.  The issue with that implementation was that, since &lt;a href=&quot;https://github.com/turbolinks/turbolinks-ios&quot; target=&quot;_blank&quot;&gt;Turbolinks for iOS&lt;/a&gt;
 manages a single shared web view, the authentication page shared the same web view in the iOS app with the rest of the pages.  This resulted in some weird navigation behaviors and required altogether too much JavaScript in the Rails side of my app to handle cases like errors on login.&lt;/p&gt;

&lt;p&gt;After doing more research and really reading through the code in &lt;a href=&quot;https://github.com/turbolinks/turbolinks-ios#running-the-demo&quot; target=&quot;_blank&quot;&gt;this Turbolinks iOS demo app&lt;/a&gt;
 I realized that the much better implementation for this flow is to have the authentication screen be in it’s own web view and to have it share a session with the rest of the app via a WKProcessPool instance that would be available to any other view that will need to be rendered after the user has been authenticated.&lt;/p&gt;

&lt;p&gt;That is a what I’m trying to do here in this post.  If you have any questions please submit a &lt;a href=&quot;https://github.com/thetizzo/thetizzo.github.io/issues&quot; target=&quot;_blank&quot;&gt;GitHub issue to this repo&lt;/a&gt;
 because this is very much a work in progress for me and maybe we can work through similar issues together. :)&lt;/p&gt;

&lt;p&gt;Here is my current implementation for the authentication process that works on web and in iOS.  It should be noted that most, if not all, of the Swift code in this post was taken from &lt;a href=&quot;https://github.com/turbolinks/turbolinks-ios/tree/master/TurbolinksDemo&quot; target=&quot;_blank&quot;&gt;this Turbolinks iOS demo app&lt;/a&gt;
 so thank you very much to the &lt;a href=&quot;https://github.com/turbolinks/turbolinks-ios/graphs/contributors&quot; target=&quot;_blank&quot;&gt;people who put that together.&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-rails-side&quot;&gt;The Rails Side&lt;/h2&gt;

&lt;p&gt;On the Rails side of this application the basic setup is to move the Devise views into &lt;code&gt;app/views/users&lt;/code&gt; and set up a custom controller for sessions that will know how to respond to JSON formatted requests.  Devise has an &lt;a href=&quot;https://github.com/plataformatec/devise#configuring-views&quot; target=&quot;_blank&quot;&gt;install generator&lt;/a&gt;
 that will copy the views for you.&lt;/p&gt;

&lt;p&gt;In this implementation, the login form’s &lt;code&gt;form_for&lt;/code&gt; does not need to have the &lt;code&gt;remote: true&lt;/code&gt; flag because it will &lt;strong&gt;not&lt;/strong&gt; need to send an XHR request when POSTing the form.  I consider this to be a feature of this implementation because it simplifies some of the cases where errors need to be handled.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# config/devise.rb
config.scoped_views = true

# config/routes.rb
devise_for :users, controllers: { sessions: &apos;sessions&apos;, registrations: &apos;registrations&apos; }

# app/controllers/sessions_controller.rb
class SessionsController &amp;lt; Devise::SessionsController
  respond_to :json
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Normally with Devise, when an unauthenticated user tries to navigate to a page Devise will respond with a 302 that redirects the user to the login page.  This will not work in iOS because a 302 means the same web view will be used to show the login page which pushes it onto the stack of navigable views. This is bad because once the user logs in they can hit a back button in the top navigation bar and end up back on the login page even though they are already authenticated.&lt;/p&gt;

&lt;p&gt;Instead, the server should recognize when the user is using the iOS app and change the format of the request to JSON so that Devise will respond with a raw 401 to the client.  Then on the iOS side we can detect the 401 error and display a separate authentication view.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/controllers/application_controller.rb
class ApplicationController &amp;lt; ActionController::Base
  # ...
  before_action :detect_browser

  def detect_browser
    case request.user_agent
    when /iPhone/i
      request.variant = :phone
      request.format = :json unless request.path.include?(Devise.mappings[:user].fullpath) || user_signed_in?
    else
      request.variant = :desktop
    end
  end
  # ...
end
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;the-ios-side&quot;&gt;The iOS Side&lt;/h2&gt;

&lt;p&gt;On the iOS side of this I basically have 3 view controllers; &lt;code&gt;ApplicationController&lt;/code&gt;, &lt;code&gt;AuthenticationController&lt;/code&gt;, and &lt;code&gt;MyViewController&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ApplicationController&lt;/code&gt; is the main controller for the application (and well named!).  It holds the host URL for the server and manages the &lt;code&gt;WKProcessPool&lt;/code&gt; instance that is used to share a session between the &lt;code&gt;AuthenticationController&lt;/code&gt; and &lt;code&gt;MyViewController&lt;/code&gt;, which is important if you want your users to stay logged in after they log in.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ApplicationController&lt;/code&gt; acts as a &lt;code&gt;SessionDelegate&lt;/code&gt;, so that it can catch errors that may happen when loading &lt;code&gt;Visitable&lt;/code&gt; views. It also acts as an &lt;code&gt;AuthenticationControllerDelegate&lt;/code&gt; so once the user has authenticated successfully, it can dismiss the &lt;code&gt;AuthenticationController&lt;/code&gt; view and load the first page of the app.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;SessionDelegate&lt;/code&gt; part of &lt;code&gt;ApplicationController&lt;/code&gt; there is a method named &lt;code&gt;session:didFailRequestForVisitable:withError&lt;/code&gt; that catches any HTTP failures and matches the response code to the case statement to see how it should handle that particular error.  This gives us a place to respond to the 401 and call &lt;code&gt;presentAuthenticationController()&lt;/code&gt; which will render the &lt;code&gt;AuthenticationController&lt;/code&gt; view.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;// ApplicationController.swift
import UIKit
import WebKit
import Turbolinks

class ApplicationController: UINavigationController {
    private let URL = NSURL(string: &quot;&amp;lt;Host URL&amp;gt;&quot;)!
    private let webViewProcessPool = WKProcessPool()

    private var application: UIApplication {
        return UIApplication.sharedApplication()
    }

    private lazy var webViewConfiguration: WKWebViewConfiguration = {
        let configuration = WKWebViewConfiguration()
        configuration.processPool = self.webViewProcessPool
        configuration.applicationNameForUserAgent = &quot;myApp&quot;
        return configuration
    }()

    private lazy var session: Session = {
        let session = Session(webViewConfiguration: self.webViewConfiguration)
        session.delegate = self
        return session
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        presentVisitableForSession(session, URL: URL)
    }

    private func presentVisitableForSession(session: Session, URL: NSURL, action: Action = .Advance) {
        let visitable = MyViewController(URL: URL)

        if action == .Advance {
            pushViewController(visitable, animated: true)
        } else if action == .Replace {
            popViewControllerAnimated(false)
            pushViewController(visitable, animated: false)
        }

        session.visit(visitable)
    }

    private func presentAuthenticationController() {
        let authenticationController = AuthenticationController()
        authenticationController.delegate = self
        authenticationController.webViewConfiguration = webViewConfiguration
        authenticationController.URL = URL.URLByAppendingPathComponent(&quot;/users/sign_in&quot;)
        authenticationController.title = &quot;Log In&quot;

        let authNavigationController = UINavigationController(rootViewController: authenticationController)
        presentViewController(authNavigationController, animated: true, completion: nil)
    }
}

extension ApplicationController: SessionDelegate {
    func session(session: Session, didProposeVisitToURL URL: NSURL, withAction action: Action) {
        presentVisitableForSession(session, URL: URL, action: action)
    }

    func session(session: Session, didFailRequestForVisitable visitable: Visitable, withError error: NSError) {
        guard let myViewController = visitable as? MyViewController, errorCode = ErrorCode(rawValue: error.code) else { return }

        switch errorCode {
        case .HTTPFailure:
            let statusCode = error.userInfo[&quot;statusCode&quot;] as! Int
            switch statusCode {
            case 401:
                presentAuthenticationController()
            default:
                myViewController.presentError(Error(HTTPStatusCode: statusCode))
            }
        case .NetworkFailure:
            myViewController.presentError(.NetworkError)
        }
    }
}

extension ApplicationController: AuthenticationControllerDelegate {
    func authenticationControllerDidAuthenticate(authenticationController: AuthenticationController) {
        session.reload()
        dismissViewControllerAnimated(true, completion: nil)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;// AuthenticationController.swift
import UIKit
import WebKit

protocol AuthenticationControllerDelegate: class {
    func authenticationControllerDidAuthenticate(authenticationController: AuthenticationController)
}

class AuthenticationController: UIViewController {
    var URL: NSURL?
    var webViewConfiguration: WKWebViewConfiguration?
    weak var delegate: AuthenticationControllerDelegate?

    lazy var webView: WKWebView = {
        let configuration = self.webViewConfiguration ?? WKWebViewConfiguration()
        let webView = WKWebView(frame: CGRectZero, configuration: configuration)
        webView.translatesAutoresizingMaskIntoConstraints = false
        webView.navigationDelegate = self
        return webView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(webView)
        view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(&quot;H:|[view]|&quot;, options: [], metrics: nil, views: [ &quot;view&quot;: webView ]))
        view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(&quot;V:|[view]|&quot;, options: [], metrics: nil, views: [ &quot;view&quot;: webView ]))

        if let URL = self.URL {
            webView.loadRequest(NSURLRequest(URL: URL))
        }
    }
}

extension AuthenticationController: WKNavigationDelegate {
    func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -&amp;gt; Void) {
        if let URL = navigationAction.request.URL where URL != self.URL {
            decisionHandler(.Cancel)
            delegate?.authenticationControllerDidAuthenticate(self)
            return
        }

        decisionHandler(.Allow)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h2&gt;

&lt;p&gt;Never publish a blog post until you’ve lived with your implementation for a bit because you may have to completely rewrite it as a result finding a better implementation. :)&lt;/p&gt;
</description>
        <pubDate>Mon, 03 Oct 2016 00:00:00 +0000</pubDate>
        <link>https://thetizzo.com/2016/10/03/authentication-with-devise-and-turbolinks-for-iOS</link>
        <guid isPermaLink="true">https://thetizzo.com/2016/10/03/authentication-with-devise-and-turbolinks-for-iOS</guid>
        
        <category>howto</category>
        
        
      </item>
    
      <item>
        <title>How I Built This Site</title>
        <description>&lt;p&gt;Once upon a time, I decided to start a blog. I’ve tried this several times in the past but the project always seems to die when it comes to figuring out what technology to use or how to host it.&lt;/p&gt;

&lt;p&gt;I’ve tried several platforms; Wordpress, Blogger, and even building the Classic Rails Blog™️ more than once. All of those solutions seem to require a ton of work just to maintain the site.  What I really want is something that is easy to set up, host, and will allow me to spend time on more interesting problems.&lt;/p&gt;

&lt;h2 id=&quot;github-pages-to-the-rescue&quot;&gt;GitHub Pages to the rescue!&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://pages.github.com/&quot; target=&quot;_blank&quot;&gt;Github Pages&lt;/a&gt;
 came to my attention and at first glance it seemed easy to use so I decided to try it out. GitHub offers an automatic page generator for Pages sites that you can access in &lt;code&gt;Settings &amp;gt; Options &amp;gt; GitHub Pages&lt;/code&gt; but this doesn’t seem to offer as much flexibility for the theme and layout of the site as I wanted.&lt;/p&gt;

&lt;p&gt;Fortunately, GitHub Pages has support for &lt;a href=&quot;http://jekyllrb.com/&quot; target=&quot;_blank&quot;&gt;Jekyll&lt;/a&gt;
 which is a static site generator with support for writing blogs in Markdown.&lt;/p&gt;

&lt;p&gt;Here’s what I had to do to make this work.&lt;/p&gt;

&lt;h2 id=&quot;setting-up-a-github-page&quot;&gt;Setting up a GitHub Page&lt;/h2&gt;

&lt;p&gt;The first thing I did to set this up was to create a git repo named &lt;code&gt;&amp;lt;your-github-username&amp;gt;.github.io&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In my case this was &lt;code&gt;thetizzo.github.io&lt;/code&gt;. It is important to make sure that your username is spelled correctly and the name of the repo matches this format or GitHub Pages will not display the site correctly.&lt;/p&gt;

&lt;p&gt;For personal GitHub Pages, the site will automatically be served using the master branch of this repository.  It’s shocking how quickly changes will show up on the actual site once you push them to GitHub.  It often doesn’t take more than a few seconds.&lt;/p&gt;

&lt;h2 id=&quot;setting-up-jekyll&quot;&gt;Setting up Jekyll&lt;/h2&gt;

&lt;p&gt;To get Jekyll set up I ran these 3 steps:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;gem install jekyll
jekyll new thetizzo.github.io
jekyll serve
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point you can go to &lt;code&gt;localhost:4000&lt;/code&gt; and see the default Jekyll site.&lt;/p&gt;

&lt;h2 id=&quot;integrating-a-theme&quot;&gt;Integrating a theme&lt;/h2&gt;

&lt;p&gt;If you want, you can start writing blogs posts right away, but I wanted to change the theme of the site make it my own. Jekyll lets you build anything so you could use Sass to design the site any way you want or you can download a pre-made theme, which are available from several places.&lt;/p&gt;

&lt;p&gt;I decided to use a pre-made theme because I figured that working through integrating that theme with my site would be a great way to gain a better understanding of how to work with Jekyll.&lt;/p&gt;

&lt;p&gt;I found &lt;a href=&quot;https://github.com/jglovier/resume-template&quot; target=&quot;_blank&quot;&gt;this theme&lt;/a&gt;
 on &lt;a href=&quot;http://jekyllthemes.org/&quot; target=&quot;_blank&quot;&gt;jekyllthemes.org&lt;/a&gt;
 and thought it was a nice, clean look that would be a good starting place for my site.&lt;/p&gt;

&lt;p&gt;This theme is set up to be it’s own standalone site so in order to integrate it I started by setting up a new page that would use a separate layout from the rest of the site.  To add a new page to the site you just need to add an HTML file to the project root. In my case this was &lt;code&gt;resume.html&lt;/code&gt; which looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;---
layout: resume
---
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That’s really it.  In Jekyll, anything at the top of a file between the dashes is called &lt;a href=&quot;https://jekyllrb.com/docs/frontmatter/&quot; target=&quot;_blank&quot;&gt;Front Matter&lt;/a&gt;
 and can be used to pass variables and defaults into a page.&lt;/p&gt;

&lt;p&gt;Since I’m using a theme, I just need the front matter for &lt;code&gt;resume.html&lt;/code&gt; to point to the &lt;code&gt;_layouts/resume.html&lt;/code&gt; layout file which will end up being the place where the bulk of the page structure is.&lt;/p&gt;

&lt;p&gt;I then copied over the rest of the theme to the appropriate places, making sure to namespace all the files to keep the two different themes (default and resume) separate until I was ready to combine them.  It looked something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;resume-template -&amp;gt; my_project

_config.yml contents -&amp;gt; _config.yml
_layouts/resume.html -&amp;gt; _layouts/resume.html
css/main.scss -&amp;gt; css/resume-main.scss
_sass/*.scss -&amp;gt; _sass/resume-*.scss
_includes/*.html -&amp;gt; _includes/resume-*.html
_includes/*.html -&amp;gt; _includes/resume-*.html
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I was then able to build a resume page using this theme while leaving the rest of the existing site alone. Once that was finished, I merged the styles from the resume page into the default layout by removing the default theme styles and removing the namespace for the resume styles and files so they would apply everywhere on the site.&lt;/p&gt;

&lt;h2 id=&quot;using-font-awesome-with-jekyll&quot;&gt;Using Font Awesome with Jekyll&lt;/h2&gt;

&lt;p&gt;I personally love &lt;a href=&quot;https://fortawesome.github.io/Font-Awesome/&quot; target=&quot;_blank&quot;&gt;Font Awesome.&lt;/a&gt;
 If you haven’t heard of Font Awesome, it’s a set of icons that you can easily include in your site.  They are included as a font to your site so you can easily customize them with CSS.&lt;/p&gt;

&lt;p&gt;Font Awesome offers a CDN to serve the assets so including them in a project is super easy.  I simply added the following line to &lt;code&gt;_includes/head.html&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;setting-up-a-custom-domain&quot;&gt;Setting up a custom domain&lt;/h2&gt;

&lt;p&gt;Another nice thing about GitHub Pages is that they offer a super easy way to put your own custom domain on the site.  This is nice because it gives you the performance benefits of GitHub’s CDN but also let’s the site have the feel of a completely custom website.&lt;/p&gt;

&lt;p&gt;I will let &lt;a href=&quot;https://help.github.com/articles/using-a-custom-domain-with-github-pages/&quot; target=&quot;_blank&quot;&gt;GitHub’s own instructions&lt;/a&gt;
 speak for themselves on this matter but basically all I had to do was add a CNAME record through my DNS provider for &lt;code&gt;thetizzo.com&lt;/code&gt; to point at &lt;code&gt;thetizzo.github.io&lt;/code&gt; and add a file called &lt;code&gt;CNAME&lt;/code&gt; to the root of the project that &lt;a href=&quot;https://github.com/thetizzo/thetizzo.github.io/blob/master/CNAME&quot; target=&quot;_blank&quot;&gt;looks like this.&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;adding-google-analytics&quot;&gt;Adding Google Analytics&lt;/h2&gt;

&lt;p&gt;To add Google Analytics to the site I created a file, &lt;code&gt;_includes/google_analytics.html&lt;/code&gt;, and put in the code snippet that Google gives you when you sign up for Analytics which looks something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script&amp;gt;
  (function(i,s,o,g,r,a,m){i[&apos;GoogleAnalyticsObject&apos;]=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,&apos;script&apos;,&apos;//www.google-analytics.com/analytics.js&apos;,&apos;ga&apos;);

  ga(&apos;create&apos;, &apos;&amp;lt;your tracking ID number&amp;gt;&apos;, &apos;auto&apos;);
  ga(&apos;send&apos;, &apos;pageview&apos;);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then included that file in &lt;code&gt;_includes/head.html&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- some head content --&amp;gt;
{% include google_analytics.html %}
&amp;lt;!-- the rest of the head content --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;have-fun&quot;&gt;Have fun!&lt;/h2&gt;

&lt;p&gt;Hopefully this was helpful.  Good luck and have fun with your new, highly advanced GitHub page.&lt;/p&gt;
</description>
        <pubDate>Mon, 29 Feb 2016 00:00:00 +0000</pubDate>
        <link>https://thetizzo.com/2016/02/29/how-i-built-this-site</link>
        <guid isPermaLink="true">https://thetizzo.com/2016/02/29/how-i-built-this-site</guid>
        
        <category>howto</category>
        
        
      </item>
    
  </channel>
</rss>
