Skip to content

Commit ba07a45

Browse files
Fix quip sync and job management (#88)
* queue mgmt * fix doc schedule * only sync if source url changes
1 parent 99e6ec4 commit ba07a45

File tree

6 files changed

+125
-50
lines changed

6 files changed

+125
-50
lines changed

app/controllers/delayed_jobs_controller.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,27 @@ class DelayedJobsController < ApplicationController
44
before_action :set_job, only: [:destroy]
55

66
def index
7-
@jobs = Delayed::Job.all
7+
@priority_filter = params[:priority]
8+
9+
@jobs = case @priority_filter
10+
when 'high'
11+
Delayed::Job.where('priority < ?', 5)
12+
when 'low'
13+
Delayed::Job.where('priority >= ?', 5)
14+
else
15+
Delayed::Job.all
16+
end
17+
18+
@jobs = @jobs.order(priority: :asc, run_at: :desc)
19+
end
20+
21+
def run_now
22+
@job = Delayed::Job.find(params[:id])
23+
24+
authorize @job, :update?
25+
26+
@job.update(run_at: Time.now)
27+
redirect_to delayed_jobs_path, notice: 'Job has been scheduled to run immediately.'
828
end
929

1030
def destroy

app/jobs/sync_quip_doc_job.rb

Lines changed: 82 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,93 @@ class SyncQuipDocJob < ApplicationJob
22
queue_as :default
33

44
def perform(_doc_id)
5-
begin
6-
document = Document.find(_doc_id)
7-
rescue ActiveRecord::RecordNotFound => e
8-
# Handle the case where the document is not found
9-
Rails.logger.error("Document with id #{_doc_id} not found: #{e.message}")
10-
return # Exit early since there's nothing to sync
11-
end
5+
document = fetch_document(_doc_id)
6+
return unless document
127

13-
# Log or handle cases where the document has no quip_url
148
if document.source_url.blank?
15-
Rails.logger.warn("Document with id #{_doc_id} has no source URL.")
9+
log_warning("Document with id #{_doc_id} has no source URL.")
1610
return
1711
end
1812

19-
begin
20-
# Initialize the Quip client
21-
quip_client = Quip::Client.new(access_token: ENV.fetch('QUIP_TOKEN'))
22-
uri = URI.parse(document.source_url)
23-
path = uri.path.sub(%r{^/}, '') # Removes the leading /
24-
quip_thread = quip_client.get_thread(path)
25-
26-
# Convert Quip HTML content to Markdown
27-
markdown_quip = ReverseMarkdown.convert(quip_thread['html'])
28-
29-
document.document = markdown_quip
30-
document.synced_at = DateTime.current
31-
document.last_sync_result = 'SUCCESS'
32-
document.save!
33-
# Reschedule the job to run again in 24 hours
34-
SyncQuipDocJob.set(wait: 24.hours, priority: 10).perform_later(_doc_id)
35-
return
36-
rescue ActiveRecord::RecordInvalid => e
37-
# Handle save! failures (validation errors)
38-
Rails.logger.error("Failed to save document with id #{_doc_id}: #{e.record.errors.full_messages.join(', ')}")
39-
document.document = "Document save failed: #{e.record.errors.full_messages.join(', ')}"
40-
rescue Quip::Error => e
41-
# Handle Quip-specific errors
42-
Rails.logger.error("Quip API error while fetching document from #{document.source_url}: #{e.message}")
43-
document.document = "Error from Quip: #{e.message}"
44-
rescue StandardError => e
45-
# Handle any other unforeseen errors
46-
Rails.logger.error("Unexpected error during sync for document id #{_doc_id}: #{e.message}")
47-
document.document = "#{e.message}"
48-
end
13+
success = sync_document_with_quip(document)
14+
update_sync_status(document, success:)
15+
schedule_next_sync(document.id, success:)
16+
end
17+
18+
private
19+
20+
def fetch_document(doc_id)
21+
Document.find(doc_id)
22+
rescue ActiveRecord::RecordNotFound => e
23+
log_error("Document with id #{doc_id} not found: #{e.message}")
24+
nil
25+
end
26+
27+
def sync_document_with_quip(document)
28+
quip_client = initialize_quip_client
29+
quip_thread = fetch_quip_thread(document.source_url, quip_client)
30+
return false unless quip_thread
31+
32+
update_document_from_quip(document, quip_thread)
33+
true
34+
rescue ActiveRecord::RecordInvalid => e
35+
handle_save_error(document, e)
36+
false
37+
rescue Quip::Error => e
38+
handle_quip_error(document, e)
39+
false
40+
rescue StandardError => e
41+
handle_unexpected_error(document, e)
42+
false
43+
end
44+
45+
def initialize_quip_client
46+
Quip::Client.new(access_token: ENV.fetch('QUIP_TOKEN'))
47+
end
48+
49+
def fetch_quip_thread(source_url, quip_client)
50+
uri = URI.parse(source_url)
51+
path = uri.path.sub(%r{^/}, '')
52+
quip_client.get_thread(path)
53+
rescue Quip::Error => e
54+
log_error("Quip API error while fetching document from #{source_url}: #{e.message}")
55+
nil
56+
end
57+
58+
def update_document_from_quip(document, quip_thread)
59+
markdown_quip = ReverseMarkdown.convert(quip_thread['html'])
60+
document.update!(document: markdown_quip, synced_at: DateTime.current, last_sync_result: 'SUCCESS')
61+
end
62+
63+
def handle_save_error(document, exception)
64+
log_error("Failed to save document with id #{document.id}: #{exception.record.errors.full_messages.join(', ')}")
65+
document.update(document: "Document save failed: #{exception.record.errors.full_messages.join(', ')}")
66+
end
67+
68+
def handle_quip_error(document, exception)
69+
log_error("Quip API error for document id #{document.id}: #{exception.message}")
70+
document.update(document: "Error from Quip: #{exception.message}")
71+
end
72+
73+
def handle_unexpected_error(document, exception)
74+
log_error("Unexpected error during sync for document id #{document.id}: #{exception.message}")
75+
document.update(document: exception.message)
76+
end
77+
78+
def update_sync_status(document, success:)
79+
document.update(synced_at: DateTime.current, last_sync_result: success ? 'SUCCESS' : 'FAILED')
80+
end
81+
82+
def schedule_next_sync(doc_id, success:)
83+
delay = success ? 24.hours : 3.hours
84+
SyncQuipDocJob.set(wait: delay, priority: 10).perform_later(doc_id)
85+
end
86+
87+
def log_error(message)
88+
Rails.logger.error(message)
89+
end
4990

50-
document.last_sync_result = 'FAILED'
51-
document.save
52-
SyncQuipDocJob.set(wait: 30.minutes, priority: 10).perform_later(_doc_id)
91+
def log_warning(message)
92+
Rails.logger.warn(message)
5393
end
5494
end

app/models/document.rb

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,19 +87,18 @@ def token_count_must_be_less_than
8787
# Sync the document with Quip if source_url is present, contains 'quip.com',
8888
def sync_quip_doc_if_needed
8989
return unless source_url.present? && source_url.include?('quip.com')
90-
91-
return unless synced_at.nil? # only schedule if it is for the initial sync
90+
return unless new_record? || saved_change_to_source_url? # Only schedule if new or source_url changed
9291

9392
# TODO: improve this dup protection
94-
return if last_sync_result == 'SCHEDULED' || last_sync_result == 'FAILED' # Prevent duplicate
95-
9693
self.last_sync_result = 'SCHEDULED'
9794

98-
SyncQuipDocJob.set(wait: 5.seconds, priority: 10).perform_later(id) # Add delay to prevent race condition with schedule jobs
95+
SyncQuipDocJob.set(wait: 5.seconds, priority: 10).perform_later(id) # Add delay to prevent race condition with scheduled jobs
9996
end
10097

10198
# Schedule the EmbedDocumentJob with a delay based on the number of jobs in the queue
10299
def schedule_embed_document_job
100+
return unless document.present?
101+
103102
total_jobs = Delayed::Job.count
104103
delay_seconds = total_jobs * 3 # 3-second delay per job in the queue
105104

app/policies/delayed/backend/active_record/job_policy.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ def initialize(user, job)
1111
@job = job
1212
end
1313

14+
def update?
15+
user.admin?
16+
end
17+
1418
def destroy?
1519
user.admin?
1620
end

app/views/delayed_jobs/index.html.erb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
<h1 class="text-2xl font-bold my-4">Delayed Jobs (<%= @jobs.count %>)</h1>
2+
<div class="flex space-x-4 mb-4">
3+
<%= link_to 'All', delayed_jobs_path, class: "px-4 py-2 border border-gray-300 rounded #{'bg-sky-800 text-white' if @priority_filter.nil?} hover:bg-gray-500 hover:text-white" %>
4+
<%= link_to 'High Priority (< 5)', delayed_jobs_path(priority: 'high'), class: "px-4 py-2 border border-gray-300 rounded #{'bg-sky-800 text-white' if @priority_filter == 'high'} hover:bg-gray-500 hover:text-white" %>
5+
<%= link_to 'Low Priority (>= 5)', delayed_jobs_path(priority: 'low'), class: "px-4 py-2 border border-gray-300 rounded #{'bg-sky-800 text-white' if @priority_filter == 'low'} hover:bg-gray-500 hover:text-white" %>
6+
</div>
27

38
<div class="flex flex-wrap -m-2">
49
<% @jobs.each do |job| %>
@@ -29,7 +34,10 @@
2934
<div class="mb-2">
3035
<strong>Created At:</strong> <%= time_ago_in_words(job.created_at) %> ago
3136
</div>
32-
<%= button_to 'Delete', delayed_job_path(job), method: :delete, data: { confirm: 'Are you sure?' }, class: "mt-auto bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded" %>
37+
<div class="flex space-x-2 mt-auto">
38+
<%= button_to 'Run Now', run_now_delayed_job_path(job), method: :post, class: "bg-white hover:bg-green-700 hover:text-white text-green-500 font-bold py-2 px-4 rounded border " %>
39+
<%= button_to 'Delete', delayed_job_path(job), method: :delete, data: { confirm: 'Are you sure?' }, class: "bg-white hover:bg-red-700 hover:text-white text-red-500 font-bold py-2 px-4 rounded border" %>
40+
</div>
3341
</div>
3442
</div>
3543
<% end %>

config/routes.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@
3838
end
3939
end
4040

41-
resources :delayed_jobs, only: %i[index destroy]
41+
resources :delayed_jobs, only: %i[index destroy] do
42+
member do
43+
post :run_now
44+
end
45+
end
4246

4347
# API Routes - Setting default format to JSON
4448
namespace :api, defaults: { format: :json } do

0 commit comments

Comments
 (0)