github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/db/db_client/db_client_execute_retry.go (about)

     1  package db_client
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"time"
     8  
     9  	"github.com/jackc/pgx/v5"
    10  	"github.com/sethvargo/go-retry"
    11  	typehelpers "github.com/turbot/go-kit/types"
    12  	"github.com/turbot/steampipe/pkg/constants"
    13  	"github.com/turbot/steampipe/pkg/db/db_common"
    14  	"github.com/turbot/steampipe/pkg/statushooks"
    15  	"github.com/turbot/steampipe/pkg/steampipeconfig"
    16  )
    17  
    18  // execute query - if it fails with a "relation not found" error, determine whether this is because the required schema
    19  // has not yet loaded and if so, wait for it to load and retry
    20  func (c *DbClient) startQueryWithRetries(ctx context.Context, session *db_common.DatabaseSession, query string, args ...any) (pgx.Rows, error) {
    21  	log.Println("[TRACE] DbClient.startQueryWithRetries start")
    22  	defer log.Println("[TRACE] DbClient.startQueryWithRetries end")
    23  
    24  	// long timeout to give refresh connections a chance to finish
    25  	maxDuration := 10 * time.Minute
    26  	backoffInterval := 250 * time.Millisecond
    27  	backoff := retry.NewConstant(backoffInterval)
    28  
    29  	conn := session.Connection.Conn()
    30  
    31  	var res pgx.Rows
    32  	count := 0
    33  	err := retry.Do(ctx, retry.WithMaxDuration(maxDuration, backoff), func(ctx context.Context) error {
    34  		count++
    35  		log.Println("[TRACE] starting", count)
    36  		rows, queryError := c.startQuery(ctx, conn, query, args...)
    37  		// if there is no error, just return
    38  		if queryError == nil {
    39  			log.Println("[TRACE] no queryError")
    40  			statushooks.SetStatus(ctx, "Loading results…")
    41  			res = rows
    42  			return nil
    43  		}
    44  
    45  		log.Println("[TRACE] queryError:", queryError)
    46  		// so there is an error - is it "relation not found"?
    47  		missingSchema, _, relationNotFound := db_common.GetMissingSchemaFromIsRelationNotFoundError(queryError)
    48  		if !relationNotFound {
    49  			log.Println("[TRACE] queryError not relation not found")
    50  			// just return it
    51  			return queryError
    52  		}
    53  
    54  		// get a connection from the system pool to query the connection state table
    55  		sysConn, err := c.managementPool.Acquire(ctx)
    56  		if err != nil {
    57  			return retry.RetryableError(err)
    58  		}
    59  		defer sysConn.Release()
    60  		// so this _was_ a "relation not found" error
    61  		// load the connection state and connection config to see if the missing schema is in there at all
    62  		// if there was a schema not found with an unqualified query, we keep trying until
    63  		// the first search path schema for each plugin has loaded
    64  		connectionStateMap, stateErr := steampipeconfig.LoadConnectionState(ctx, sysConn.Conn(), steampipeconfig.WithWaitUntilLoading())
    65  		if stateErr != nil {
    66  			log.Println("[TRACE] could not load connection state map:", stateErr)
    67  			// just return the query error
    68  			return queryError
    69  		}
    70  
    71  		// if there are no connections, just return the error
    72  		if len(connectionStateMap) == 0 {
    73  			log.Println("[TRACE] no data in connection state map")
    74  			return queryError
    75  		}
    76  
    77  		// is this an unqualified query...
    78  		if missingSchema == "" {
    79  			log.Println("[TRACE] this was an unqualified query")
    80  			// refresh the search path, as now the connection state is in loading state, search paths may have been updated
    81  			if err := c.ensureSessionSearchPath(ctx, session); err != nil {
    82  				return queryError
    83  			}
    84  
    85  			// we need the first search path connection for each plugin to be loaded
    86  			searchPath := c.GetRequiredSessionSearchPath()
    87  			requiredConnections := connectionStateMap.GetFirstSearchPathConnectionForPlugins(searchPath)
    88  			// if required connections are ready (and have been for more than the backoff interval) , just return the relation not found error
    89  			if connectionStateMap.Loaded(requiredConnections...) && time.Since(connectionStateMap.ConnectionModTime()) > backoffInterval {
    90  				return queryError
    91  			}
    92  
    93  			// otherwise we need to wait for the first schema of everything plugin to load
    94  			if _, err := steampipeconfig.LoadConnectionState(ctx, sysConn.Conn(), steampipeconfig.WithWaitForSearchPath(searchPath)); err != nil {
    95  				return err
    96  			}
    97  
    98  			// so now the connections are loaded - retry the query
    99  			return retry.RetryableError(queryError)
   100  		}
   101  
   102  		// so a schema was specified
   103  		// verify it exists in the connection state and is not disabled
   104  		connectionState, missingSchemaExistsInStateMap := connectionStateMap[missingSchema]
   105  		if !missingSchemaExistsInStateMap {
   106  			log.Println("[TRACE] schema", missingSchema, "is not in schema map")
   107  			//, missing schema is not in connection state map - just return the error
   108  			return queryError
   109  		}
   110  
   111  		// so schema _is_ in the state map
   112  		if connectionState.Disabled() {
   113  			log.Println("[TRACE] schema", missingSchema, "is disabled")
   114  			return queryError
   115  		}
   116  
   117  		// if the connection is ready (and has been for more than the backoff interval) , just return the relation not found error
   118  		if connectionState.State == constants.ConnectionStateReady && time.Since(connectionState.ConnectionModTime) > backoffInterval {
   119  			log.Println("[TRACE] schema", missingSchema, "has been ready for a long time")
   120  			return queryError
   121  		}
   122  
   123  		// if connection is in error,return the connection error
   124  		if connectionState.State == constants.ConnectionStateError {
   125  			log.Println("[TRACE] schema", missingSchema, "is in error")
   126  			return fmt.Errorf("connection %s failed to load: %s", missingSchema, typehelpers.SafeString(connectionState.ConnectionError))
   127  		}
   128  
   129  		// ok so we will retry
   130  		// build the status message to display with a spinner, if needed
   131  		statusMessage := steampipeconfig.GetLoadingConnectionStatusMessage(connectionStateMap, missingSchema)
   132  		statushooks.SetStatus(ctx, statusMessage)
   133  		return retry.RetryableError(queryError)
   134  	})
   135  
   136  	return res, err
   137  }