워크플로 마이그레이션 문제

안녕하세요, 워크플로우가 활성화되어 있지 않은데도 로그에 항목이 계속 쌓이는 것 같습니다.

Discourse | v2026.7.0-latest +394

Job exception: PG::UndefinedTable: ERROR: relation “discourse_workflows_webhooks” does not exist LINE 10: WHERE a.attrelid = ‘“discourse_workflows_webhooks”’::regcla…

메시지 (252회 보고됨)

Job exception: PG::UndefinedTable: ERROR: relation “discourse_workflows_webhooks” does not exist
LINE 10: WHERE a.attrelid = ‘“discourse_workflows_webhooks”’::regcla…

^

Backtrace

rack-mini-profiler-4.0.1/lib/patches/db/pg/alias_method.rb:109:in 'PG::Connection#exec'
rack-mini-profiler-4.0.1/lib/patches/db/pg/alias_method.rb:109:in 'PG::Connection#async_exec'
activerecord-8.0.5/lib/active_record/connection_adapters/postgresql/database_statements.rb:167:in 'ActiveRecord::ConnectionAdapters::PostgreSQL::DatabaseStatements#perform_query'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract/database_statements.rb:556:in 'block (2 levels) in ActiveRecord::ConnectionAdapters::DatabaseStatements#raw_execute'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract_adapter.rb:1022:in 'block in ActiveRecord::ConnectionAdapters::AbstractAdapter#with_raw_connection'
activesupport-8.0.5/lib/active_support/concurrency/null_lock.rb:9:in 'ActiveSupport::Concurrency::NullLock#synchronize'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract_adapter.rb:991:in 'ActiveRecord::ConnectionAdapters::AbstractAdapter#with_raw_connection'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract/database_statements.rb:555:in 'block in ActiveRecord::ConnectionAdapters::DatabaseStatements#raw_execute'
activesupport-8.0.5/lib/active_support/notifications/instrumenter.rb:58:in 'ActiveSupport::Notifications::Instrumenter#instrument'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract_adapter.rb:1142:in 'ActiveRecord::ConnectionAdapters::AbstractAdapter#log'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract/database_statements.rb:554:in 'ActiveRecord::ConnectionAdapters::DatabaseStatements#raw_execute'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract/database_statements.rb:591:in 'ActiveRecord::ConnectionAdapters::DatabaseStatements#internal_execute'
activerecord-8.0.5/lib/active_record/connection_adapters/postgresql/database_statements.rb:15:in 'ActiveRecord::ConnectionAdapters::PostgreSQL::DatabaseStatements#query'
activerecord-8.0.5/lib/active_record/connection_adapters/postgresql_adapter.rb:1042:in 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#column_definitions'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract/schema_statements.rb:109:in 'ActiveRecord::ConnectionAdapters::SchemaStatements#columns'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:345:in 'block (2 levels) in ActiveRecord::ConnectionAdapters::SchemaCache#columns'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract/connection_pool.rb:416:in 'ActiveRecord::ConnectionAdapters::ConnectionPool#with_connection'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:344:in 'block in ActiveRecord::ConnectionAdapters::SchemaCache#columns'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:343:in 'Hash#fetch'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:343:in 'ActiveRecord::ConnectionAdapters::SchemaCache#columns'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:354:in 'block in ActiveRecord::ConnectionAdapters::SchemaCache#columns_hash'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:353:in 'Hash#fetch'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:353:in 'ActiveRecord::ConnectionAdapters::SchemaCache#columns_hash'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:54:in 'ActiveRecord::ConnectionAdapters::SchemaReflection#columns_hash'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:198:in 'ActiveRecord::ConnectionAdapters::BoundSchemaReflection#columns_hash'
activerecord-8.0.5/lib/active_record/model_schema.rb:592:in 'ActiveRecord::ModelSchema::ClassMethods#load_schema!'
activerecord-8.0.5/lib/active_record/counter_cache.rb:187:in 'ActiveRecord::CounterCache::ClassMethods#load_schema!'
activerecord-8.0.5/lib/active_record/encryption/encryptable_record.rb:127:in 'ActiveRecord::Encryption::EncryptableRecord::ClassMethods#load_schema!'
activerecord-8.0.5/lib/active_record/model_schema.rb:539:in 'block in ActiveRecord::ModelSchema::ClassMethods#load_schema'
activerecord-8.0.5/lib/active_record/model_schema.rb:536:in 'Monitor#synchronize'
activerecord-8.0.5/lib/active_record/model_schema.rb:536:in 'ActiveRecord::ModelSchema::ClassMethods#load_schema'
activerecord-8.0.5/lib/active_record/model_schema.rb:428:in 'ActiveRecord::ModelSchema::ClassMethods#columns_hash'
activerecord-8.0.5/lib/active_record/attributes.rb:255:in 'block in ActiveRecord::Attributes::ClassMethods#_default_attributes'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract/connection_pool.rb:422:in 'ActiveRecord::ConnectionAdapters::ConnectionPool#with_connection'
activerecord-8.0.5/lib/active_record/connection_handling.rb:312:in 'ActiveRecord::ConnectionHandling#with_connection'
activerecord-8.0.5/lib/active_record/attributes.rb:254:in 'ActiveRecord::Attributes::ClassMethods#_default_attributes'
activemodel-8.0.5/lib/active_model/attribute_registration.rb:38:in 'ActiveModel::AttributeRegistration::ClassMethods#attribute_types'
activemodel-8.0.5/lib/active_model/attribute_registration.rb:49:in 'ActiveModel::AttributeRegistration::ClassMethods#type_for_attribute'
activerecord-8.0.5/lib/active_record/type_caster/map.rb:16:in 'ActiveRecord::TypeCaster::Map#type_for_attribute'
activerecord-8.0.5/lib/arel/table.rb:107:in 'Arel::Table#type_for_attribute'
activerecord-8.0.5/lib/active_record/table_metadata.rb:18:in 'ActiveRecord::TableMetadata#type'
activerecord-8.0.5/lib/active_record/relation/predicate_builder.rb:59:in 'ActiveRecord::PredicateBuilder#build'
activerecord-8.0.5/lib/active_record/relation/predicate_builder.rb:54:in 'ActiveRecord::PredicateBuilder#
'
activerecord-8.0.5/lib/active_record/relation/predicate_builder.rb:145:in 'block in ActiveRecord::PredicateBuilder#expand_from_hash'
activerecord-8.0.5/lib/active_record/relation/predicate_builder.rb:87:in 'Hash#each'
activerecord-8.0.5/lib/active_record/relation/predicate_builder.rb:87:in 'Enumerable#flat_map'
activerecord-8.0.5/lib/active_record/relation/predicate_builder.rb:87:in 'ActiveRecord::PredicateBuilder#expand_from_hash'
activerecord-8.0.5/lib/active_record/relation/predicate_builder.rb:25:in 'ActiveRecord::PredicateBuilder#build_from_hash'
activerecord-8.0.5/lib/active_record/relation/query_methods.rb:1643:in 'ActiveRecord::QueryMethods#build_where_clause'
activerecord-8.0.5/lib/active_record/relation/query_methods.rb:1044:in 'ActiveRecord::QueryMethods#where!'
activerecord-8.0.5/lib/active_record/relation/query_methods.rb:1039:in 'ActiveRecord::QueryMethods#where'
/var/www/discourse/plugins/discourse-workflows/app/models/discourse_workflows/webhook.rb:12:in 'block in class:Webhook'
activerecord-8.0.5/lib/active_record/relation.rb:565:in 'BasicObject#instance_exec'
activerecord-8.0.5/lib/active_record/relation.rb:565:in 'block in ActiveRecord::Relation#_exec_scope'
activerecord-8.0.5/lib/active_record/relation.rb:1383:in 'ActiveRecord::Relation#_scoping'
activerecord-8.0.5/lib/active_record/relation.rb:565:in 'ActiveRecord::Relation#_exec_scope'
activerecord-8.0.5/lib/active_record/scoping/named.rb:175:in 'block in DiscourseWorkflows::Webhook.scope'
/var/www/discourse/plugins/discourse-workflows/lib/discourse_workflows/webhook_test_listener.rb:88:in 'DiscourseWorkflows::WebhookTestListener.purge_expired!'
/var/www/discourse/plugins/discourse-workflows/app/jobs/scheduled/discourse_workflows/purge_expired_webhook_test_listeners.rb:9:in 'Jobs::DiscourseWorkflows::PurgeExpiredWebhookTestListeners#execute'
/var/www/discourse/app/jobs/base.rb:313:in 'block (2 levels) in Jobs::Base#perform'
rails_multisite-7.0.0/lib/rails_multisite/connection_management/null_instance.rb:49:in 'RailsMultisite::ConnectionManagement::NullInstance#with_connection'
rails_multisite-7.0.0/lib/rails_multisite/connection_management.rb:17:in 'RailsMultisite::ConnectionManagement.with_connection'
/var/www/discourse/app/jobs/base.rb:300:in 'block in Jobs::Base#perform'
/var/www/discourse/app/jobs/base.rb:297:in 'Array#each'
/var/www/discourse/app/jobs/base.rb:297:in 'Jobs::Base#perform'
/var/www/discourse/app/jobs/base.rb:375:in 'Jobs::Scheduled#perform'
mini_scheduler-0.20.0/lib/mini_scheduler/manager.rb:137:in 'MiniScheduler::Manager::Runner#process_queue'
mini_scheduler-0.20.0/lib/mini_scheduler/manager.rb:77:in 'MiniScheduler::Manager::Runner#worker_loop'
mini_scheduler-0.20.0/lib/mini_scheduler/manager.rb:63:in 'block (2 levels) in MiniScheduler::Manager::Runner#ensure_worker_threads'




플러그인이 설치되어 있는 것으로 보이지만, 해당 플러그인이 사용하는 필수 테이블을 생성하기 위한 마이그레이션을 실행하지 않은 것 같습니다.

플러그인을 설치하는 컨테이너 정의의 해당 부분을 보여 주실 수 있을까요?

감사합니다. 다만 discourse-workflows는 제가 설치한 것이 아니라 번들된 코어 플러그인(plugins/에 포함되며, Discourse에서 제작하고 enable_discourse_workflows 플래그 뒤에 위치)이라 app.yml에 해당 설정이 없습니다.

해당 테이블은 db/migrate/20260312000000_create_workflow_tables.rb 파일의 212번째 줄에 있는 마이그레이션으로 discourse_workflows_webhooks 테이블을 생성하도록 되어 있지만, 제 데이터베이스에서는 실행되지 않았습니다.

주요 이유는 정기 작업(PurgeExpiredWebhookTestListeners)에 가드가 있는지 확인하고 싶기 때문입니다. 테이블이 없는 경우 에러가 반복되는 대신 조용히 무시되도록 처리할 가치가 있는지 궁금합니다.

1개의 좋아요

테이블 discourse_workflows_webhooks을 생성하는 마이그레이션(create_workflow_tables, 버전 20260312000000)은 존재하며, 해당 파일 하단 근처에 create_table이 위치해 있습니다.

제 데이터베이스에서는 이 마이그레이션이 이미 schema_migrations에 실행된 것으로 기록되어 있지만, 웹훅 테이블 자체는 생성되지 않았습니다.

클린한 db:migrate 실행으로 이를 확인했습니다. 아무 출력 없이 실행되어 대기 중인 마이그레이션이 없었지만, 테이블은 여전히 존재하지 않았습니다. Rails는 버전 번호를 기준으로 마이그레이션이 완료된 것으로 보고 다시 실행하지 않으므로, 모델과 예약된 정화(purge) 작업은 계속 로드되고 실행되지만 테이블은 누락된 상태로 남습니다. 이것이 동일한 작업 예외가 계속 쌓이던 이유입니다.

적용한 수정 방법: 마이그레이션이 이미 기록되어 있어 다시 실행할 수 없었습니다. 기존에 존재하는 다른 워크플로 테이블을 다시 생성하려고 시도하여 실패할 것이기 때문입니다.

따라서 누락된 테이블을 직접 생성했습니다. 마이그레이션과 정확히 일치하도록(동일한 컬럼, 고유 인덱스를 포함한 동일한 5개의 인덱스) 생성했습니다:

conn = ActiveRecord::Base.connection
unless conn.table_exists?(:discourse_workflows_webhooks)
conn.create_table :discourse_workflows_webhooks do |t|
t.bigint :workflow_id, null: false
t.string :workflow_version_id, limit: 36
t.string :node_name, null: false, limit: 100
t.string :webhook_path, null: false, limit: 500
t.string :http_method, null: false, limit: 10
t.string :webhook_id, limit: 36
t.integer :path_length
t.boolean :test_webhook, null: false, default: false
t.integer :user_id
t.jsonb :workflow_snapshot
t.datetime :expires_at
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
end
conn.add_index :discourse_workflows_webhooks, %i[http_method webhook_path test_webhook], unique: true, name: "idx_dwf_webhooks_on_method_path_test"
conn.add_index :discourse_workflows_webhooks, %i[webhook_id http_method test_webhook], name: "idx_dwf_webhooks_on_webhook_id_method_test", where: "webhook_id IS NOT NULL"
conn.add_index :discourse_workflows_webhooks, :workflow_id, name: "idx_dwf_webhooks_on_workflow_id"
conn.add_index :discourse_workflows_webhooks, :workflow_version_id, name: "idx_dwf_webhooks_on_workflow_version_id"
conn.add_index :discourse_workflows_webhooks, :expires_at, name: "idx_dwf_webhooks_on_expires_at", where: "expires_at IS NOT NULL"
end

이후 table_exists?는 true를 반환하고, PurgeExpiredWebhookTestListeners 작업은 다음 실행 시 더 이상 오류를 내지 않습니다.

이 문제는 이미 실행된 후 편집된 배포된 마이그레이션에서 비롯된 것입니다. 따라서 이 테이블에 대한 적절한 마이그레이션을 추가하는 후속 코어 업데이트가 테이블을 다시 생성하려고 시도하여 “already exists” 오류로 실패할 수 있습니다.

업데이트 후 이러한 문제가 발생하면, 새 마이그레이션이 우선하도록 해야 합니다. 즉, 수동으로 생성한 테이블을 먼저 삭제하거나, 해당 버전의 마이그레이션이 건너뛰어지도록 schema_migrations에 그 버전을 삽입해야 합니다.

최신 브랜치에서 동일한 작업 예외에 직면하는 다른 사용자가 있을 수 있으므로 이 내용을 공유합니다.

3개의 좋아요