github.com/wfusion/gofusion@v1.1.14/common/infra/watermill/pubsub/sql/publisher.go (about) 1 package sql 2 3 import ( 4 "context" 5 "sync" 6 7 "github.com/pkg/errors" 8 9 "github.com/wfusion/gofusion/common/infra/watermill" 10 "github.com/wfusion/gofusion/common/infra/watermill/message" 11 ) 12 13 var ( 14 ErrPublisherClosed = errors.New("publisher is closed") 15 ) 16 17 type PublisherConfig struct { 18 // SchemaAdapter provides the schema-dependent queries and arguments for them, based on topic/message etc. 19 SchemaAdapter SchemaAdapter 20 21 // AutoInitializeSchema enables initialization of schema database during publish. 22 // Schema is initialized once per topic per publisher instance. 23 // AutoInitializeSchema is forbidden if using an ongoing transaction as database handle; 24 // That could result in an implicit commit of the transaction by a CREATE TABLE statement. 25 AutoInitializeSchema bool 26 27 AppID string 28 } 29 30 func (c PublisherConfig) validate() error { 31 if c.SchemaAdapter == nil { 32 return errors.New("schema adapter is nil") 33 } 34 35 return nil 36 } 37 38 func (c *PublisherConfig) setDefaults() { 39 } 40 41 // Publisher inserts the Messages as rows into a SQL table.. 42 type Publisher struct { 43 config PublisherConfig 44 45 db ContextExecutor 46 47 publishWg *sync.WaitGroup 48 closeCh chan struct{} 49 closed bool 50 51 initializedTopics sync.Map 52 logger watermill.LoggerAdapter 53 } 54 55 func NewPublisher(db ContextExecutor, config PublisherConfig, logger watermill.LoggerAdapter) (*Publisher, error) { 56 config.setDefaults() 57 if err := config.validate(); err != nil { 58 return nil, errors.Wrap(err, "invalid config") 59 } 60 61 if db == nil { 62 return nil, errors.New("db is nil") 63 } 64 65 if logger == nil { 66 logger = watermill.NopLogger{} 67 } 68 69 if config.AutoInitializeSchema && isTx(db) { 70 // either use a prior schema with a tx db handle, or don't use tx with AutoInitializeSchema 71 return nil, errors.New("tried to use AutoInitializeSchema with a database handle that looks like" + 72 "an ongoing transaction; this may result in an implicit commit") 73 } 74 75 return &Publisher{ 76 config: config, 77 db: db, 78 79 publishWg: new(sync.WaitGroup), 80 closeCh: make(chan struct{}), 81 closed: false, 82 83 logger: logger, 84 }, nil 85 } 86 87 // Publish inserts the messages as rows into the MessagesTable. 88 // Order is guaranteed for messages within one call. 89 // Publish is blocking until all rows have been added to the Publisher's transaction. 90 // Publisher doesn't guarantee publishing messages in a single transaction, 91 // but the constructor accepts both *sql.DB and *sql.Tx, so transactions may be handled upstream by the user. 92 func (p *Publisher) Publish(ctx context.Context, topic string, messages ...*message.Message) (err error) { 93 if p.closed { 94 return ErrPublisherClosed 95 } 96 97 p.publishWg.Add(1) 98 defer p.publishWg.Done() 99 100 if err := validateTopicName(topic); err != nil { 101 return err 102 } 103 104 if err := p.initializeSchema(topic); err != nil { 105 return err 106 } 107 108 for _, msg := range messages { 109 msg.Metadata[watermill.MessageHeaderAppID] = p.config.AppID 110 } 111 112 insertQuery, insertArgs, err := p.config.SchemaAdapter.InsertQuery(topic, messages) 113 if err != nil { 114 return errors.Wrap(err, "cannot create insert query") 115 } 116 117 p.logger.Trace("[Common] watermill inserting message to SQL", watermill.LogFields{ 118 "query": insertQuery, 119 // "query_args": sqlArgsToLog(insertArgs), 120 }) 121 122 _, err = p.db.ExecContext(ctx, insertQuery, insertArgs...) 123 if err != nil { 124 return errors.Wrap(err, "could not insert message as row") 125 } 126 127 return nil 128 } 129 130 func (p *Publisher) initializeSchema(topic string) error { 131 if !p.config.AutoInitializeSchema { 132 return nil 133 } 134 135 if _, ok := p.initializedTopics.Load(topic); ok { 136 return nil 137 } 138 139 if err := initializeSchema( 140 context.Background(), 141 topic, 142 p.logger, 143 p.db, 144 p.config.SchemaAdapter, 145 nil, 146 ); err != nil { 147 return errors.Wrap(err, "cannot initialize schema") 148 } 149 150 p.initializedTopics.Store(topic, struct{}{}) 151 return nil 152 } 153 154 // Close closes the publisher, which means that all the Publish calls called before are finished 155 // and no more Publish calls are accepted. 156 // Close is blocking until all the ongoing Publish calls have returned. 157 func (p *Publisher) Close() error { 158 if p.closed { 159 return nil 160 } 161 162 p.closed = true 163 164 close(p.closeCh) 165 p.publishWg.Wait() 166 167 return nil 168 } 169 170 func isTx(db ContextExecutor) bool { 171 _, dbIsTx := db.(interface { 172 Commit() error 173 Rollback() error 174 }) 175 return dbIsTx 176 }