<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Development on GeekyHub</title>
    <link>https://www.geekyhub.in/tags/development/</link>
    <description>Recent content in Development on GeekyHub</description>
    <generator>Hugo -- 0.148.1</generator>
    <language>en</language>
    <lastBuildDate>Sun, 20 Jul 2025 12:56:46 +0530</lastBuildDate>
    <atom:link href="https://www.geekyhub.in/tags/development/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Unlocking Performance with AsyncJob: Specific Use Cases for I/O-Bound Operations</title>
      <link>https://www.geekyhub.in/post/elasticsearch-opensearch-indexing-performance-rails-jobs/</link>
      <pubDate>Sun, 20 Jul 2025 12:26:17 +0530</pubDate>
      <guid>https://www.geekyhub.in/post/elasticsearch-opensearch-indexing-performance-rails-jobs/</guid>
      <description>Discover how I achieved 5x faster Elasticsearch operations in Rails by switching to AsyncJob. The benchmarks show dramatic performance improvements with 30% less memory usage.</description>
      <content:encoded><![CDATA[<h2 id="the-elasticsearch-scaling-nightmare">The Elasticsearch Scaling Nightmare</h2>
<p>Picture this: Your Rails application is humming along nicely until suddenly, user growth explodes. Great news, right? Not for our job queue.</p>
<p>In a previous application I worked on, we faced significant challenges scaling ActiveJobs, primarily due to numerous Elasticsearch indexing and other I/O-bound tasks. As our user base grew, thousands of small jobs overwhelmed our queue, creating a perfect storm of bottlenecks despite our best efforts at dynamic worker scaling.</p>
<p>We tried all the usual solutions:</p>
<ul>
<li>Increased worker counts (which quickly hit diminishing returns)</li>
<li>Added more powerful servers (hello, spiraling AWS bills)</li>
<li>Optimized database connections (still not enough)</li>
<li>Tweaked job priorities and batching strategies (marginal improvements at best)</li>
</ul>
<p>Yet we still hit hard limits with database connections, memory consumption, and infrastructure costs. The fundamental issue wasn&rsquo;t our implementation,it was the thread-based job processing paradigm itself, which simply wasn&rsquo;t optimized for our I/O-heavy workload.</p>
<h2 id="why-asyncjob-for-io-bound-operations">Why AsyncJob for I/O-Bound Operations?</h2>
<p>Enter <a href="https://github.com/socketry/async-job">AsyncJob</a>, a gem that fundamentally reimagines how Ruby handles concurrent operations by leveraging <strong>Ruby fibers</strong> for cooperative multitasking.</p>
<h3 id="the-thread-vs-fiber-difference">The Thread vs. Fiber Difference</h3>
<p>To understand why this matters, let&rsquo;s break down what happens during an Elasticsearch indexing operation:</p>
<p><strong>With traditional thread-based job processors (like SolidQueue):</strong></p>
<ol>
<li>A thread makes an HTTP request to Elasticsearch</li>
<li>The thread blocks while waiting for a response</li>
<li>The OS must context-switch to another thread</li>
<li>Each thread requires its own memory stack (often 1MB+ per thread)</li>
<li>Thread context switching has significant overhead</li>
</ol>
<p><strong>With fiber-based AsyncJob:</strong></p>
<ol>
<li>A fiber makes an HTTP request to Elasticsearch</li>
<li>The fiber voluntarily yields control while waiting</li>
<li>Another fiber can immediately use the same thread</li>
<li>Fibers are lightweight (a few KB each)</li>
<li>Switching between fibers is extremely efficient</li>
</ol>
<p>This approach is exceptionally efficient for operations that spend most of their time waiting for external resources (I/O-bound), because instead of wasting CPU cycles waiting for responses, the system can process other jobs in the meantime.</p>
<h2 id="getting-started-with-asyncjob">Getting Started with AsyncJob</h2>
<p>Integrating AsyncJob into your Rails application is surprisingly straightforward. The gem provides an adapter for ActiveJob, allowing you to selectively apply it to jobs that would benefit most from fiber-based processing.</p>
<h3 id="installation">Installation</h3>
<p>Add these gems to your Gemfile:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>gem <span style="color:#e6db74">&#39;async-job&#39;</span>
</span></span><span style="display:flex;"><span>gem <span style="color:#e6db74">&#39;async-job-adapter-active_job&#39;</span>
</span></span><span style="display:flex;"><span>gem <span style="color:#e6db74">&#39;async-job-processor-redis&#39;</span>
</span></span></code></pre></div><p>Then run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>bundle install
</span></span></code></pre></div><h3 id="configuration">Configuration</h3>
<p>For jobs that would benefit from fiber-based processing, simply specify AsyncJob as the queue adapter:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SearchIndexingJob</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ApplicationJob</span>
</span></span><span style="display:flex;"><span>  self<span style="color:#f92672">.</span>queue_adapter <span style="color:#f92672">=</span> <span style="color:#e6db74">:async_job</span>  <span style="color:#75715e"># This job uses fibers</span>
</span></span><span style="display:flex;"><span>  queue_as <span style="color:#e6db74">:default</span>
</span></span><span style="display:flex;"><span>  
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">perform</span>(document)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Your I/O-heavy code here</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>You can continue using your existing queue adapter for other jobs:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CPUIntensiveJob</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ApplicationJob</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># This job still uses your default queue adapter</span>
</span></span><span style="display:flex;"><span>  queue_as <span style="color:#e6db74">:default</span>
</span></span><span style="display:flex;"><span>  
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">perform</span>(data)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># CPU-intensive operations</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>For more detailed setup instructions, check out the <a href="https://socketry.github.io/async-job-adapter-active_job/guides/getting-started/index">AsyncJob Active Job Adapter documentation</a>.</p>
<h2 id="benchmarking-asyncjob-vs-solidqueue-for-opensearch-operations">Benchmarking AsyncJob vs. SolidQueue for OpenSearch Operations</h2>
<p>To validate my hypothesis about AsyncJob&rsquo;s benefits for I/O-bound operations, I created a comprehensive benchmarking project comparing AsyncJob (fiber-based) with SolidQueue (thread-based) for OpenSearch indexing operations.</p>
<h3 id="the-experiment-setup">The Experiment Setup</h3>
<p>I created two nearly identical jobs:</p>
<ul>
<li><code>JobTest1Job</code> - Using SolidQueue with synchronous HTTP calls</li>
<li><code>JobTest2Job</code> - Using AsyncJob with asynchronous HTTP calls</li>
</ul>
<p>Both jobs perform the same task: indexing documents in OpenSearch. To simulate real-world conditions with network latency, I added a 3-second artificial delay to each OpenSearch request, results were drastically different. But the results here are without any simulated delay.</p>
<p>The full code and detailed results are available in my <a href="https://github.com/vikas-0/jobtest">GitHub repository</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f92672">==========</span> BENCHMARK RESULTS <span style="color:#f92672">==========</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>----- Batch Size: <span style="color:#ae81ff">10</span> documents -----
</span></span><span style="display:flex;"><span>SolidQueue <span style="color:#f92672">(</span>JobTest1Job<span style="color:#f92672">)</span>:
</span></span><span style="display:flex;"><span>  Avg Enqueue Time: 0.18s
</span></span><span style="display:flex;"><span>  Avg Total Time: 1.19s
</span></span><span style="display:flex;"><span>  Avg Throughput: 8.45 docs/second
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>AsyncJob <span style="color:#f92672">(</span>JobTest2Job<span style="color:#f92672">)</span>:
</span></span><span style="display:flex;"><span>  Avg Enqueue Time: 0.06s
</span></span><span style="display:flex;"><span>  Avg Total Time: 1.06s
</span></span><span style="display:flex;"><span>  Avg Throughput: 9.39 docs/second
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Result: AsyncJob was 11.6% faster <span style="color:#66d9ef">for</span> <span style="color:#ae81ff">10</span> documents
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>----- Batch Size: <span style="color:#ae81ff">100</span> documents -----
</span></span><span style="display:flex;"><span>SolidQueue <span style="color:#f92672">(</span>JobTest1Job<span style="color:#f92672">)</span>:
</span></span><span style="display:flex;"><span>  Avg Enqueue Time: 0.59s
</span></span><span style="display:flex;"><span>  Avg Total Time: 1.6s
</span></span><span style="display:flex;"><span>  Avg Throughput: 62.96 docs/second
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>AsyncJob <span style="color:#f92672">(</span>JobTest2Job<span style="color:#f92672">)</span>:
</span></span><span style="display:flex;"><span>  Avg Enqueue Time: 0.24s
</span></span><span style="display:flex;"><span>  Avg Total Time: 1.25s
</span></span><span style="display:flex;"><span>  Avg Throughput: 80.12 docs/second
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Result: AsyncJob was 27.4% faster <span style="color:#66d9ef">for</span> <span style="color:#ae81ff">100</span> documents
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>----- Batch Size: <span style="color:#ae81ff">1000</span> documents -----
</span></span><span style="display:flex;"><span>SolidQueue <span style="color:#f92672">(</span>JobTest1Job<span style="color:#f92672">)</span>:
</span></span><span style="display:flex;"><span>  Avg Enqueue Time: 4.8s
</span></span><span style="display:flex;"><span>  Avg Total Time: 19.17s
</span></span><span style="display:flex;"><span>  Avg Throughput: 54.78 docs/second
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>AsyncJob <span style="color:#f92672">(</span>JobTest2Job<span style="color:#f92672">)</span>:
</span></span><span style="display:flex;"><span>  Avg Enqueue Time: 2.25s
</span></span><span style="display:flex;"><span>  Avg Total Time: 3.25s
</span></span><span style="display:flex;"><span>  Avg Throughput: 307.48 docs/second
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Result: AsyncJob was 489.1% faster <span style="color:#66d9ef">for</span> <span style="color:#ae81ff">1000</span> documents
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">========================================</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">==========</span> RESOURCE USAGE METRICS <span style="color:#f92672">==========</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>----- SOLID_QUEUE Resource Usage -----
</span></span><span style="display:flex;"><span>Memory Usage:
</span></span><span style="display:flex;"><span>  Average: 128.69 MB
</span></span><span style="display:flex;"><span>  Maximum: 177.19 MB
</span></span><span style="display:flex;"><span>CPU Usage:
</span></span><span style="display:flex;"><span>  Average per job: 28.4549 seconds
</span></span><span style="display:flex;"><span>  Total CPU time: 74295.6439 seconds
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>----- ASYNC_JOB Resource Usage -----
</span></span><span style="display:flex;"><span>Memory Usage:
</span></span><span style="display:flex;"><span>  Average: 89.42 MB
</span></span><span style="display:flex;"><span>  Maximum: 118.83 MB
</span></span><span style="display:flex;"><span>CPU Usage:
</span></span><span style="display:flex;"><span>  Average per job: 3.128 seconds
</span></span><span style="display:flex;"><span>  Total CPU time: 5864.9192 seconds
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>----- Comparison -----
</span></span><span style="display:flex;"><span>Memory Usage: AsyncJob uses -30.51% less memory than SolidQueue
</span></span><span style="display:flex;"><span>CPU Usage: AsyncJob uses -92.11% less CPU time than SolidQueue
</span></span></code></pre></div><p>Note: For solid queue, I have used 1 worker with 3 threads, which is a default configuration for solid queue.</p>
<h2 id="analyzing-the-results">Analyzing the Results</h2>
<h3 id="performance-scaling-with-batch-size">Performance Scaling with Batch Size</h3>
<p>The most striking finding from the benchmarks is how AsyncJob&rsquo;s advantage scales with batch size:</p>
<table>
  <thead>
      <tr>
          <th>Batch Size</th>
          <th>Performance Advantage</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>10 documents</td>
          <td>AsyncJob was 11.6% faster</td>
      </tr>
      <tr>
          <td>100 documents</td>
          <td>AsyncJob was 27.4% faster</td>
      </tr>
      <tr>
          <td>1000 documents</td>
          <td>AsyncJob was 489.1% faster</td>
      </tr>
  </tbody>
</table>
<p><strong>Key Insight:</strong> AsyncJob&rsquo;s performance advantage dramatically increases with the number of I/O operations. For small workloads, the difference is noticeable but modest. For large workloads, the difference is transformative.</p>
<h3 id="resource-efficiency">Resource Efficiency</h3>
<p>Beyond raw speed, the resource efficiency gains are equally impressive:</p>
<ul>
<li><strong>Memory Usage:</strong> AsyncJob uses 30.5% less memory than SolidQueue (89.42 MB vs 128.69 MB)</li>
<li><strong>CPU Usage:</strong> AsyncJob uses 92.1% less CPU time than SolidQueue (5,865 seconds vs 74,296 seconds)</li>
</ul>
<p>This translates to significant infrastructure cost savings at scale.</p>
<h2 id="the-secret-sauce-async-http-client">The Secret Sauce: Async HTTP Client</h2>
<p>A key part of my implementation was creating a fiber-aware HTTP client for OpenSearch. Here&rsquo;s a simplified version:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AsyncOpenSearchClient</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">index</span>(<span style="color:#e6db74">index</span>:, id: <span style="color:#66d9ef">nil</span>, <span style="color:#e6db74">body</span>:)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Return an Async task that performs the request asynchronously</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">Async</span> <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>task<span style="color:#f92672">|</span>  
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Make the request asynchronously</span>
</span></span><span style="display:flex;"><span>      response <span style="color:#f92672">=</span> <span style="color:#66d9ef">Async</span><span style="color:#f92672">::</span><span style="color:#66d9ef">HTTP</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Internet</span><span style="color:#f92672">.</span>post(url, headers, json_body)
</span></span><span style="display:flex;"><span>      
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Process response...</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>This client ensures that while waiting for HTTP responses, other fibers can continue processing, maximizing throughput.</p>
<h2 id="when-to-use-asyncjob">When to Use AsyncJob</h2>
<p>Based on my benchmarks and some research (Google and AI), AsyncJob is ideal for:</p>
<h3 id="1-search-engine-indexing">1. Search Engine Indexing</h3>
<p>This was our primary pain point. AsyncJob transforms Elasticsearch/OpenSearch indexing by efficiently switching fibers during network I/O waits, allowing concurrent indexing operations.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SearchIndexingJob</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ApplicationJob</span>
</span></span><span style="display:flex;"><span>  self<span style="color:#f92672">.</span>queue_adapter <span style="color:#f92672">=</span> <span style="color:#e6db74">:async_job</span>
</span></span><span style="display:flex;"><span>  queue_as <span style="color:#e6db74">:default</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">perform</span>(document)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Each index operation is an I/O operation where AsyncJob can yield</span>
</span></span><span style="display:flex;"><span>    search_client<span style="color:#f92672">.</span>index(
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">index</span>: <span style="color:#e6db74">&#34;content&#34;</span>,
</span></span><span style="display:flex;"><span>      id: document<span style="color:#f92672">.</span>id,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">body</span>: document<span style="color:#f92672">.</span>to_indexed_json
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><h3 id="2-api-integrations">2. API Integrations</h3>
<p>Jobs that make external API calls are perfect candidates for AsyncJob:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">NotificationJob</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ApplicationJob</span>
</span></span><span style="display:flex;"><span>  self<span style="color:#f92672">.</span>queue_adapter <span style="color:#f92672">=</span> <span style="color:#e6db74">:async_job</span>
</span></span><span style="display:flex;"><span>  queue_as <span style="color:#e6db74">:default</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">perform</span>(user_id, message)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Multiple API calls can run concurrently</span>
</span></span><span style="display:flex;"><span>    push_notification_service<span style="color:#f92672">.</span>send(user_id, message)
</span></span><span style="display:flex;"><span>    slack_service<span style="color:#f92672">.</span>notify(message)
</span></span><span style="display:flex;"><span>    analytics_service<span style="color:#f92672">.</span>track(<span style="color:#e6db74">&#34;notification_sent&#34;</span>, user_id)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><h3 id="3-llm-operations">3. LLM Operations</h3>
<p>Large Language Model API calls typically have high latency, making them ideal for AsyncJob:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AIProcessingJob</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ApplicationJob</span>
</span></span><span style="display:flex;"><span>  self<span style="color:#f92672">.</span>queue_adapter <span style="color:#f92672">=</span> <span style="color:#e6db74">:async_job</span>
</span></span><span style="display:flex;"><span>  queue_as <span style="color:#e6db74">:default</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">perform</span>(documents)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Process multiple documents concurrently</span>
</span></span><span style="display:flex;"><span>    documents<span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>doc<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>      summary <span style="color:#f92672">=</span> llm_client<span style="color:#f92672">.</span>summarize(doc<span style="color:#f92672">.</span>content)
</span></span><span style="display:flex;"><span>      doc<span style="color:#f92672">.</span>update(<span style="color:#e6db74">ai_summary</span>: summary)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><h2 id="when-not-to-use-asyncjob">When NOT to Use AsyncJob</h2>
<ol>
<li>
<p><strong>CPU-Intensive Operations</strong>: If your job is CPU-bound (complex calculations, image processing), traditional thread-based queues may perform better.</p>
</li>
<li>
<p><strong>Very Short-Running Jobs</strong>: For jobs that complete in milliseconds with minimal I/O, the overhead of fiber management might not be worth it.</p>
</li>
<li>
<p><strong>Jobs with Non-Fiber-Aware Dependencies</strong>: Some gems and libraries aren&rsquo;t fiber-aware and may block fibers, negating AsyncJob&rsquo;s benefits.</p>
</li>
<li>
<p><strong>Jobs Requiring Robust Error Handling</strong>: AsyncJob currently lacks some of the error handling capabilities found in more mature job processors.</p>
</li>
</ol>
<h2 id="important-considerations-error-handling-with-asyncjob">Important Considerations: Error Handling with AsyncJob</h2>
<p>While AsyncJob offers impressive performance benefits, it&rsquo;s important to be aware of its current limitations in error handling:</p>
<ol>
<li>Limited Built-in Error Handling</li>
<li>Manual Error Logging Required</li>
<li>Monitoring Challenges</li>
</ol>
<h2 id="conclusion-a-game-changer-for-io-heavy-workloads">Conclusion: A Game-Changer for I/O-Heavy Workloads</h2>
<p>For applications with significant I/O-bound job processing needs, AsyncJob represents a paradigm shift in performance and efficiency. My benchmarks show that it&rsquo;s not just marginally better—it can be 5x faster while using 30% less memory and 90% less CPU time.</p>
<p>The best part? You can gradually adopt AsyncJob for specific jobs without changing your entire infrastructure. Start by identifying your most I/O-intensive jobs and convert them first to see immediate benefits.</p>
<p>If your Rails application is struggling with I/O-bound job processing, AsyncJob might just be the solution you&rsquo;ve been looking for.</p>
<p>(Note: I have not used it extensively in production yet, but the benchmarks show promising results.)</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to Use Tiptap&#39;s Collaboration Feature with Rails Action Cable</title>
      <link>https://www.geekyhub.in/post/implementing-a-google-doc-notion-like-collborative-editor-in-rails-react-tiptap/</link>
      <pubDate>Tue, 18 Jun 2024 20:55:00 +0530</pubDate>
      <guid>https://www.geekyhub.in/post/implementing-a-google-doc-notion-like-collborative-editor-in-rails-react-tiptap/</guid>
      <description>Create a collaborative text editor like Notion using Tiptap for rich text, ReactJS for frontend, and Rails with Action Cable for real-time updates. Enable simultaneous editing and instant synchronization across users for a seamless collaborative experience.</description>
      <content:encoded><![CDATA[<p>In this post, we&rsquo;ll walk through setting up Tiptap&rsquo;s collaboration feature with Rails Action Cable and ReactJs. Tiptap is a powerful headless editor built on ProseMirror, and when combined with Y.js, it allows for real-time collaborative editing. We&rsquo;ll use Mantine component library, but it&rsquo;s not mandatory for this setup.</p>
<p>If you prefer to dive directly into the code, check out the example on <a href="https://github.com/vikas-0/collab_demo">Github</a></p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube-nocookie.com/embed/HXpudWU5FxQ?autoplay=0&amp;controls=1&amp;end=0&amp;loop=1&amp;mute=0&amp;playlist=HXpudWU5FxQ&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<h3 id="prerequisites">Prerequisites</h3>
<p>Ensure you have the following installed:</p>
<ul>
<li>Ruby on Rails</li>
<li>Redis</li>
<li>Node.js and Yarn</li>
<li>Your preferred mehtod of React Setup with Rails</li>
</ul>
<h3 id="step-1-setting-up-mantine">Step 1: Setting Up Mantine</h3>
<p>First, we&rsquo;ll set up Mantine for styling. Follow the <a href="https://mantine.dev/guides/vite/">Mantine guide for Vite</a> to install the necessary packages: (Same method worked for me using esbuild in my setup. You can do it you own way or choose not to use Mantine)</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>yarn add @mantine/core @mantine/hooks @mantine/tiptap @tabler/icons-react @tiptap/react @tiptap/extension-link @tiptap/starter-kit @tiptap/extension-placeholder @tiptap/extension-collaboration-cursor @tiptap/extension-collaboration yjs y-prosemirror
</span></span><span style="display:flex;"><span>yarn add --dev postcss postcss-preset-mantine postcss-simple-vars
</span></span></code></pre></div><blockquote>
<p>Note: This setup includes both Mantine and Tiptap. If you do not require Mantine, skip installing Mantine-related dependencies.</p></blockquote>
<h3 id="step-2-install-rails-dependencies">Step 2: Install Rails Dependencies</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>bundle add redis y-rb_actioncable y-rb
</span></span></code></pre></div><p>Here we are installing Y.js adapter for Ruby and Action Cable.</p>
<h3 id="step-3-configure-tiptap-with-collaboration">Step 3: Configure Tiptap with Collaboration</h3>
<p>In the Tiptap setup, configure the StarterKit with history: false as the Collaboration extension comes with its own history management. Additionally, we’ll add a random color generator for collaboration cursors.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getRandomColor</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">colors</span> <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;#ff901f&#34;</span>, <span style="color:#e6db74">&#34;#ff2975&#34;</span>, <span style="color:#e6db74">&#34;#f222ff&#34;</span>, <span style="color:#e6db74">&#34;#8c1eff&#34;</span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">selectedIndex</span> <span style="color:#f92672">=</span> Math.<span style="color:#a6e22e">floor</span>(Math.<span style="color:#a6e22e">random</span>() <span style="color:#f92672">*</span> (<span style="color:#a6e22e">colors</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>));
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">colors</span>[<span style="color:#a6e22e">selectedIndex</span>];
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">editor</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">useEditor</span>({
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">extensions</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">StarterKit</span>.<span style="color:#a6e22e">configure</span>({ <span style="color:#a6e22e">history</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span> }),
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Underline</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Link</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Superscript</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">SubScript</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Highlight</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">TextAlign</span>.<span style="color:#a6e22e">configure</span>({ <span style="color:#a6e22e">types</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;heading&#39;</span>, <span style="color:#e6db74">&#39;paragraph&#39;</span>] }),
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Placeholder</span>.<span style="color:#a6e22e">configure</span>({ <span style="color:#a6e22e">placeholder</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;This is placeholder&#39;</span> }),
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Collaboration</span>.<span style="color:#a6e22e">configure</span>({
</span></span><span style="display:flex;"><span>                document<span style="color:#f92672">:</span> <span style="color:#a6e22e">doc</span> <span style="color:#75715e">// Configure Y.Doc for collaboration
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            }),
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">CollaborationCursor</span>.<span style="color:#a6e22e">configure</span>({
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">provider</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">user</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;Vikas&#34;</span>,
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">color</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">getRandomColor</span>()
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            })
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>    });
</span></span></code></pre></div><p>Code to connect with websocket provided by ActionCable. Don&rsquo;t worry about the channel creation now, we will create it later. Assuming channel name will be <code>SyncChannel</code> we will add following code. (Here id is hardcoded, as this is just a demo. we won&rsquo;t be using proper auth in backend as well to keep things simple)</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#75715e">// ... other imports
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">createConsumer</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@rails/actioncable&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">WebsocketProvider</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@y-rb/actioncable&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">consumer</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">createConsumer</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">doc</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Y</span>.<span style="color:#a6e22e">Doc</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">provider</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">WebsocketProvider</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">doc</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">consumer</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;SyncChannel&#34;</span>,
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">id</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ... other codes
</span></span></span></code></pre></div><p>You can see full frontend code in <a href="https://github.com/vikas-0/collab_demo/blob/main/app/javascript/App.jsx">App.jsx</a>. This contains everything in a single file which is not great but good enough for this case.</p>
<h3 id="step-4-set-up-rails-action-cable">Step 4: Set Up Rails Action Cable</h3>
<p>Create a new channel name <code>SyncChannel</code> at <code>app/channels/sync_channel.rb</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># frozen_string_literal: true</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SyncChannel</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ApplicationCable</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Channel</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> Y<span style="color:#f92672">::</span><span style="color:#66d9ef">Actioncable</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Sync</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">subscribed</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># initiate sync &amp; subscribe to updates, with optional persistence mechanism</span>
</span></span><span style="display:flex;"><span>    sync_for(session) { <span style="color:#f92672">|</span>id, update<span style="color:#f92672">|</span> save_doc(id, update) }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">receive</span>(message)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># broadcast update to all connected clients on all servers</span>
</span></span><span style="display:flex;"><span>    sync_to(session, message)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">doc</span>
</span></span><span style="display:flex;"><span>    @doc <span style="color:#f92672">||=</span> load { <span style="color:#f92672">|</span>id<span style="color:#f92672">|</span> load_doc(id) }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">private</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">session</span>
</span></span><span style="display:flex;"><span>    @session <span style="color:#f92672">||=</span> <span style="color:#66d9ef">Session</span><span style="color:#f92672">.</span>new(params<span style="color:#f92672">[</span><span style="color:#e6db74">:id</span><span style="color:#f92672">]</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">load_doc</span>(id)
</span></span><span style="display:flex;"><span>    data <span style="color:#f92672">=</span> <span style="color:#66d9ef">REDIS</span><span style="color:#f92672">.</span>get(id)
</span></span><span style="display:flex;"><span>    data <span style="color:#f92672">=</span> data<span style="color:#f92672">.</span>unpack(<span style="color:#e6db74">&#34;C*&#34;</span>) <span style="color:#66d9ef">unless</span> data<span style="color:#f92672">.</span>nil?
</span></span><span style="display:flex;"><span>    data
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">save_doc</span>(id, state)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">REDIS</span><span style="color:#f92672">.</span>set(id, state<span style="color:#f92672">.</span>pack(<span style="color:#e6db74">&#34;C*&#34;</span>))
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>This has Redis initialized as REDIS, replace it with your Redis variable name. We also created a Session model for <code>sync_for</code> mehtod. You can check documentation for sync_for <a href="https://y-crdt.github.io/yrb-actioncable/Y/Actioncable/Sync.html#sync_for-instance_method">here</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># frozen_string_literal: true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Session</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">attr_reader</span> <span style="color:#e6db74">:id</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">initialize</span>(id)
</span></span><span style="display:flex;"><span>    @id <span style="color:#f92672">=</span> id
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">to_s</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;sessions:</span><span style="color:#e6db74">#{</span>id<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>And finally <code>ApplicationCable::Connection</code> will be as follows</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">module</span> ApplicationCable
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Connection</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActionCable</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Connection</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>    identified_by <span style="color:#e6db74">:id</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">connect</span>
</span></span><span style="display:flex;"><span>      self<span style="color:#f92672">.</span>id <span style="color:#f92672">=</span> <span style="color:#66d9ef">SecureRandom</span><span style="color:#f92672">.</span>uuid
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><h3 id="step-6-add-styles-for-collaboration-cursor-option">Step 6: Add Styles for Collaboration Cursor (Option)</h3>
<p>Everything should be working by now. In this step the cursor was looking odd, so some <a href="https://github.com/vikas-0/collab_demo/blob/main/app/javascript/App.css">CSS</a> can be add to make it look good.</p>
<p>Finally you can run your rails server and it should be good to go once we add all missing piecies specially authorisation.</p>
<h3 id="conclusion">Conclusion</h3>
<p>By following these steps, you should have a real-time collaborative editor up and running using Tiptap, Y.js, and Rails Action Cable. While we used Mantine for styling in this demo, you can customize the styling as per your requirements. This setup provides a robust foundation for building collaborative applications with rich text editing capabilities.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
