Class: CollavreLinear::OutboundCommentSyncJob
- Inherits:
-
ApplicationJob
- Object
- ApplicationJob
- CollavreLinear::OutboundCommentSyncJob
- Defined in:
- app/jobs/collavre_linear/outbound_comment_sync_job.rb
Overview
Pushes a single Collavre::Comment to its linked Linear issue as a comment.
CollavreLinear::OutboundCommentSyncJob.perform_later(comment.id)
Idempotent: the IssueLink row is locked and the CommentLink existence is re-checked inside the lock, so concurrent performs (or a duplicate enqueue) never post the same chat message to Linear twice.
Instance Method Summary collapse
Instance Method Details
#perform(comment_id) ⇒ Object
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'app/jobs/collavre_linear/outbound_comment_sync_job.rb', line 16 def perform(comment_id) comment = ::Collavre::Comment.find(comment_id) issue_link = comment.creative&.linear_issue_links&.first return if issue_link&.linear_issue_id.blank? account = issue_link.project_link.account return unless account # Captured inside the lock so the self-echo rescue can reconcile AFTER the # lock's transaction rolls back — rescuing the unique violation inside the # `with_lock` transaction would poison it (Postgres aborts the whole tx). posted = nil begin issue_link.with_lock do # Re-check under the lock: an inbound mirror or a prior run may have # linked this comment already. One Collavre comment -> one Linear comment. return if CollavreLinear::CommentLink.exists?(comment_id: comment.id) # Re-check visibility at run time: the comment was public/Main when the # observer enqueued this create, but may have been made private or moved # out of Main since. No CommentLink exists yet, so the observer cannot # enqueue a delete for that change — posting the now-hidden body would # leak it. Same predicate the update job re-runs; here it must no-op. return unless CollavreLinear::CommentSyncability.syncable?(comment) posted = CollavreLinear::Client.new(account).create_comment( issue_id: issue_link.linear_issue_id, body: CollavreLinear::CommentFormatter.outbound_body(comment) ) return if posted[:id].blank? # Record Linear's updatedAt for this version so the inbound applier can # recognise the create webhook (and any stale echo of it) as our own by # timestamp rather than by comparing against the mutable local body. CollavreLinear::CommentLink.create!( comment_id: comment.id, linear_comment_id: posted[:id], issue_link: issue_link, remote_updated_at: posted[:updatedAt] ) end rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e # Self-echo race: the inbound create webhook for the comment we just posted # landed first (app_actor_id is nil, so EchoGuard can't drop it), mirrored # it locally, and grabbed the unique linear_comment_id. Adopt that link onto # our original comment and drop the duplicate mirror — otherwise the comment # is double-represented and our original stays unlinked, so a later edit # re-posts it. Mirrors CreativeExporter#reconcile_self_echo!. Any other # validation error must still surface. raise if e.is_a?(ActiveRecord::RecordInvalid) && !duplicate_comment_link_error?(e) reconcile_comment_self_echo!(comment, issue_link, posted) end rescue ActiveRecord::RecordNotFound Rails.logger.info( "[CollavreLinear::OutboundCommentSyncJob] Comment #{comment_id} not found; skipping" ) end |