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  }