简体   繁体   English

Go 例程后关闭冗余 sql.Rows 对象的推荐方式

[英]Recommended way of closing redundant sql.Rows object after Go routine

I'm using Go routines to send queries to PostgreSQL master and slave nodes in parallel.我正在使用 Go 例程将查询并行发送到 PostgreSQL 主节点和从节点。 The first host that returns a valid result wins.返回有效结果的第一个主机获胜。 Error cases are outside the scope of this question.错误案例不在此问题的范围内。

The caller is the only one that cares about the contents of a *sql.Rows object, so intentionally my function doesn't do any operations on those.调用者是唯一关心*sql.Rows对象内容的人,所以我的函数有意不对这些内容进行任何操作。 I use buffered channels to retrieve return objects from the Go routines, so there should be no Go routine leak.我使用缓冲通道从 Go 例程中检索返回对象,因此应该没有 Go 例程泄漏。 Garbage collection should take care of the rest.垃圾收集应该处理剩下的事情。

There is a problem I haven't taught about properly: the Rows objects that remain behind in the channel are never closed.有一个我没有正确教过的问题:留在通道后面的 Rows 对象永远不会关闭。 When I call this function from a (read only) transaction, tx.Rollback() returns an error for every instance of non-closed Rows object: "unexpected command tag SELECT" .当我从(只读)事务调用此函数时, tx.Rollback()为非关闭的Rows对象的每个实例返回一个错误: "unexpected command tag SELECT"

This function is called from higher level objects:这个函数是从更高级别的对象调用的:

func multiQuery(ctx context.Context, xs []executor, query string, args ...interface{}) (*sql.Rows, error) {
    rc := make(chan *sql.Rows, len(xs))
    ec := make(chan error, len(xs))
    for _, x := range xs {
        go func(x executor) {
            rows, err := x.QueryContext(ctx, query, args...)
            switch { // Make sure only one of them is returned
            case err != nil:
                ec <- err
            case rows != nil:
                rc <- rows
            }
        }(x)
    }

    var me MultiError
    for i := 0; i < len(xs); i++ {
        select {
        case err := <-ec:
            me.append(err)
        case rows := <-rc: // Return on the first success
            return rows, nil
        }
    }
    return nil, me.check()
}

Executors can be *sql.DB , *sql.Tx or anything that complies with the interface: Executors 可以是*sql.DB*sql.Tx或任何符合接口的东西:

type executor interface {
    ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
    QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
    QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
}

Rollback logic:回滚逻辑:

func (mtx MultiTx) Rollback() error {
    ec := make(chan error, len(mtx))
    for _, tx := range mtx {
        go func(tx *Tx) {
            err := tx.Rollback()
            ec <- err
        }(tx)
    }
    var me MultiError
    for i := 0; i < len(mtx); i++ {
        if err := <-ec; err != nil {
            me.append(err)
        }
    }
    return me.check()
}

MultiTx is a collection of open transactions on multiple nodes. MultiTx是多个节点上的开放交易的集合。 It is a higher level object that calls multiQuery它是一个更高级别的对象,它调用multiQuery

What would be the best approach to "clean up" unused rows? “清理”未使用行的最佳方法是什么? Options I'm thinking about not doing:我正在考虑不做的选项:

  1. Cancel the context: I believe it will work inconsistently, multiple queries might already have returned by the time cancel() is called取消上下文:我相信它会不一致地工作,在调用cancel()时可能已经返回了多个查询
  2. Create a deferred Go routine which continues to drain the channels and close the rows objects: If a DB node is slow to respond, Rollback() is still called before rows.Close()创建一个延迟的 Go 例程,它继续排空通道并关闭行对象:如果数据库节点响应缓慢,则仍会rows.Close()之前调用Rollback() rows.Close()
  3. Use a sync.WaitGroup somewhere in the MultiTx type, maybe in combination with (2): This can cause Rollback to hang if one of the nodes is unresponsive.在 MultiTx 类型中的某处使用sync.WaitGroup ,可能与 (2) 结合使用:如果其中一个节点没有响应,这可能会导致回滚挂起。 Also, I wouldn't be sure how I would implement that.另外,我不确定我将如何实现它。
  4. Ignore the Rollback errors: Ignoring errors never sounds like a good idea, they are there for a reason.忽略回滚错误:忽略错误听起来从来都不是一个好主意,它们存在是有原因的。

What would be the recommended way of approaching this?解决这个问题的推荐方法是什么?

Edit:编辑:

As suggested by @Peter, I've tried canceling the context, but it seems this also invalidates all the returned Rows from the query.正如@Peter 所建议的,我尝试取消上下文,但这似乎也使查询中所有返回的行无效。 On rows.Scan I'm getting context canceled error at the higher level caller.rows.Scan我在更高级别的调用者处收到context canceled错误。

This is what I've done so far:这是我到目前为止所做的:

func multiQuery(ctx context.Context, xs []executor, query string, args ...interface{}) (*sql.Rows, error) {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    rc := make(chan *sql.Rows, len(xs))
    ec := make(chan error, len(xs))
    for _, x := range xs {
        go func(x executor) {
            rows, err := x.QueryContext(ctx, query, args...)
            switch { // Make sure only one of them is returned
            case err != nil:
                ec <- err
            case rows != nil:
                rc <- rows
                cancel() // Cancel on success
            }
        }(x)
    }

    var (
        me   MultiError
        rows *sql.Rows
    )
    for i := 0; i < len(xs); i++ {
        select {
        case err := <-ec:
            me.append(err)
        case r := <-rc:
            if rows == nil { // Only use the first rows
                rows = r
            } else {
                r.Close() // Cleanup remaining rows, if there are any
            }
        }
    }
    if rows != nil {
        return rows, nil
    }

    return nil, me.check()
}

Edit 2:编辑2:

@Adrian mentioned: @Adrian 提到:

we can't see the code that's actually using any of this.我们看不到实际使用这些的代码。

This code is reused by type methods.此代码由类型方法重用。 First there is the transaction type.首先是交易类型。 The issues in this question are appearing on the Rollback() method above.这个问题的问题出现在上面的Rollback()方法上。

// MultiTx holds a slice of open transactions to multiple nodes.
// All methods on this type run their sql.Tx variant in one Go routine per Node.
type MultiTx []*Tx

// QueryContext runs sql.Tx.QueryContext on the tranactions in separate Go routines.
// The first non-error result is returned immediately
// and errors from the other Nodes will be ignored.
//
// If all nodes respond with the same error, that exact error is returned as-is.
// If there is a variety of errors, they will be embedded in a MultiError return.
//
// Implements boil.ContextExecutor.
func (mtx MultiTx) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
    return multiQuery(ctx, mtx2Exec(mtx), query, args...)
}

Then there is:然后是:

// MultiNode holds a slice of Nodes.
// All methods on this type run their sql.DB variant in one Go routine per Node.
type MultiNode []*Node

// QueryContext runs sql.DB.QueryContext on the Nodes in separate Go routines.
// The first non-error result is returned immediately
// and errors from the other Nodes will be ignored.
//
// If all nodes respond with the same error, that exact error is returned as-is.
// If there is a variety of errors, they will be embedded in a MultiError return.
//
// Implements boil.ContextExecutor.
func (mn MultiNode) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
    return multiQuery(ctx, nodes2Exec(mn), query, args...)
}

These methods the public wrappers around the multiQuery() function.这些方法是围绕multiQuery()函数的公共包装器。 Now I realize that just sending the *Rows into a buffered channel to die, is actually a memory leak.现在我意识到只是将*Rows发送到缓冲通道中去死,实际上是内存泄漏。 In the transaction cases it becomes clear, as Rollback() starts to complain.在事务情况下,很明显,因为Rollback()开始抱怨。 But in the non-transaction variant, the *Rows inside the channel will never be garbage collected, as the driver might hold reference to it until rows.Close() is called.但是在非事务变体中,通道内的*Rows永远不会被垃圾收集,因为驱动程序可能会保留对它的引用,直到rows.Close()为止。

I've written this package to by used by an ORM, sqlboiler .我已经编写了这个包,供 ORM sqlboiler 使用 My higher level logic passes a MultiTX object to the ORM.我的高层逻辑将MultiTX对象传递给 ORM。 From that point, I don't have any explicit control over the returned Rows .从那时起,我对返回的Rows没有任何明确的控制。 A simplistic approach would be that my higher level code cancels the context before Rollback() , but I don't like that:一个简单的方法是我的更高级别的代码在Rollback()之前取消上下文,但我不喜欢这样:

  1. It gives a non-intuitive API.它提供了一个非直观的 API。 This (idiomatic) approach would break:这种(惯用的)方法会中断:
ctx, cancel = context.WithCancel(context.Background())
defer cancel()
tx, _ := db.BeginTx(ctx)
defer tx.Rollback()

  1. The ORM's interfaces also specify the regular, non-context aware Query() variants, which in my package's case will run against context.Background() . ORM 的接口还指定了常规的、非上下文感知的Query()变体,在我的包的情况下,它将针对context.Background()运行。

I'm starting to worry that this broken by design... Anyway, I will start by implementing a Go routine that will drain the channel and close the *Rows .我开始担心这被设计破坏了......无论如何,我将首先实现一个 Go 例程,该例程将耗尽通道并关闭*Rows After that I will see if I can implement some reasonable waiting / cancellation mechanism that won't affect the returned *Rows之后我会看看我是否可以实现一些合理的等待/取消机制,不会影响返回的*Rows

I think that the function below will do what you require with the one provisio being that the context passed in should be cancelled when you are done with the results (otherwise one context.WithCancel will leak; I cannot see a way around that as cancelling it within the function will invalidate the returned sql.Rows ).我认为下面的函数可以满足您的要求,前提是当您完成结果时应该取消传入的上下文(否则一个context.WithCancel会泄漏;我看不到取消它的方法)在函数内将使返回的sql.Rows无效)。

Note that I have not had time to test this (would need to setup a database, implement your interfaces etc) so there may well be a bug hidden in the code (but I believe the basic algorithm is sound)请注意,我没有时间对此进行测试(需要设置数据库、实现接口等),因此代码中可能隐藏了一个错误(但我相信基本算法是合理的)

// queryResult holds the goroutine# and the result from that gorouting (need both so we can avoid cancelling the relevant context)
type queryResult struct {
    no   int
    rows *sql.Rows
}

// multiQuery - Executes multiple queries and returns either the first to resutn a result or, if all fail, a multierror summarising the errors
// Important: This should be used for READ ONLY queries only (it is possible that more than one will complete)
// Note: The ctx passed in must be cancelled to avoid leaking a context (this routine cannot cancel the context used for the winning query)
func multiQuery(ctx context.Context, xs []executor, query string, args ...interface{}) (*sql.Rows, error) {
    noOfQueries := len(xs)
    rc := make(chan queryResult) // Channel for results; unbuffered because we only want one, and only one, result
    ec := make(chan error)       // errors get sent here - goroutines must send a result or 1 error
    defer close(ec)              // Ensure the error consolidation go routine will complete

    // We need a way to cancel individual goroutines as we do not know which one will succeed
    cancelFns := make([]context.CancelFunc, noOfQueries)

    // All goroutines must terminate before we exit (otherwise the transaction maybe rolled back before they are cancelled leading to "unexpected command tag SELECT")
    var wg sync.WaitGroup
    wg.Add(noOfQueries)

    for i, x := range xs {
        var queryCtx context.Context
        queryCtx, cancelFns[i] = context.WithCancel(ctx)
        go func(ctx context.Context, queryNo int, x executor) {
            defer wg.Done()

            rows, err := x.QueryContext(ctx, query, args...)
            if err != nil {
                ec <- err // Error collection go routine guaranteed to run until all query goroutines complete
                return
            }

            select {
            case rc <- queryResult{queryNo, rows}:
                return
            case <-ctx.Done(): // If another query has already transmitted its results these should be thrown away
                rows.Close() // not strictly required because closed context should tidy up
                return
            }
        }(queryCtx, i, x)
    }

    // Start go routine that will send a MultiError to a channel if all queries fail
    mec := make(chan MultiError)
    go func() {
        var me MultiError
        errCount := 0
        for err := range ec {
            me.append(err)
            errCount += 1
            if errCount == noOfQueries {
                mec <- me
                return
            }

        }
    }()

    // Wait for one query to succeed or all queries to fail
    select {
    case me := <-mec:
        for _, cancelFn := range cancelFns { // not strictly required so long as ctx is eventually cancelled
            cancelFn()
        }
        wg.Wait()
        return nil, me.check()
    case result := <-rc:
        for i, cancelFn := range cancelFns { // not strictly required so long as ctx is eventually cancelled
            if i != result.no { // do not cancel the query that returned a result
                cancelFn()
            }
        }
        wg.Wait()
        return result.rows, nil
    }
}

Thanks to the comments from @Peter and the answer of @Brits, I got fresh ideas on how to approach this.感谢@Peter 的评论和@Brits 的回答,我对如何解决这个问题有了新的想法。

Blue print蓝图

3 out of 4 proposals from the question were needed to be implemented.该问题的 4 项建议中有 3 项需要实施。

1. Cancel the Context 1.取消上下文

mtx.QueryContext() creates a descendant context and sets the CancelFunc in the MultiTx object. mtx.QueryContext()创建一个后代上下文并设定CancelFuncMultiTx对象。

The cancelWait() helper cancels an old context and waits for MultiTX.Done if its not nil. cancelWait()助手取消旧的上下文并等待MultiTX.Done如果它不为零。 It is called on Rollback() and before every new query.它在Rollback()和每个新查询之前被调用。

2. Drain the channel 2. 排空通道

In multiQuery() , Upon obtaining the first successful Rows , a Go routine is launched to drain and close the remaining Rows .multiQuery() ,在获得第一个成功的Rows ,会启动一个 Go 例程来排空和关闭剩余的Rows The rows channel no longer needs to be buffered.行通道不再需要缓冲。

An additional Go routine and a WaitGroup is used to close the error and rows channels.额外的 Go 例程和WaitGroup用于关闭错误和行通道。

3. Return a done channel 3.返回一个完成的频道

Instead of the proposed WaitGroup , multiQuery() returns a done channel. multiQuery()返回一个 done 通道,而不是建议的WaitGroup The channel is closed once the drain & close routine has finished.一旦排放和关闭程序完成,通道就会关闭。 mtx.QueryContext() sets done the channel on the MultiTx object. mtx.QueryContext()MultiTx对象上设置完成通道。

Errors错误

Instead of the select block, only drain the error channel if there are now Rows .如果现在有Rows ,则只排空错误通道,而不是select块。 The error needs to remain buffered for this reason.由于这个原因,错误需要保持缓冲。

Code代码

// MultiTx holds a slice of open transactions to multiple nodes.
// All methods on this type run their sql.Tx variant in one Go routine per Node.
type MultiTx struct {
    tx      []*Tx
    done    chan struct{}
    cancels context.CancelFunc
}

func (m *MultiTx) cancelWait() {
    if m.cancel != nil {
        m.cancel()
    }
    if m.done != nil {
        <-m.done
    }

    // reset
    m.done, m.cancel = nil, nil
}

// Context creates a child context and appends CancelFunc in MultiTx
func (m *MultiTx) context(ctx context.Context) context.Context {
    m.cancelWait()
    ctx, m.cancel = context.WithCancel(ctx)
    return ctx
}

// QueryContext runs sql.Tx.QueryContext on the tranactions in separate Go routines.
func (m *MultiTx) QueryContext(ctx context.Context, query string, args ...interface{}) (rows *sql.Rows, err error) {
    rows, m.done, err = multiQuery(m.context(ctx), mtx2Exec(m.tx), query, args...)
    return rows, err
}

func (m *MultiTx) Rollback() error {
    m.cancelWait()
    ec := make(chan error, len(m.tx))
    for _, tx := range m.tx {
        go func(tx *Tx) {
            err := tx.Rollback()
            ec <- err
        }(tx)
    }
    var me MultiError
    for i := 0; i < len(m.tx); i++ {
        if err := <-ec; err != nil {
            me.append(err)
        }
    }
    return me.check()
}

func multiQuery(ctx context.Context, xs []executor, query string, args ...interface{}) (*sql.Rows, chan struct{}, error) {
    rc := make(chan *sql.Rows)
    ec := make(chan error, len(xs))

    var wg sync.WaitGroup
    wg.Add(len(xs))
    for _, x := range xs {
        go func(x executor) {
            rows, err := x.QueryContext(ctx, query, args...)
            switch { // Make sure only one of them is returned
            case err != nil:
                ec <- err
            case rows != nil:
                rc <- rows
            }
            wg.Done()
        }(x)
    }

    // Close channels when all query routines completed
    go func() {
        wg.Wait()
        close(ec)
        close(rc)
    }()

    rows, ok := <-rc
    if ok { // ok will be false if channel closed before any rows
        done := make(chan struct{}) // Done signals the caller that all remaining rows are properly closed
        go func() {
            for rows := range rc { // Drain channel and close unused Rows
                rows.Close()
            }
            close(done)
        }()
        return rows, done, nil
    }

    // no rows, build error return
    var me MultiError
    for err := range ec {
        me.append(err)
    }
    return nil, nil, me.check()
}

Edit: Cancel & wait for old contexts before every Query, as *sql.Tx is not Go routine save, all previous queries have to be done before a next call.编辑:在每次查询之前取消并等待旧上下文,因为*sql.Tx不是 Go 例程保存,所有先前的查询都必须在下一次调用之前完成。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM