-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
engine: create a max procedure call stack depth #609
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,7 @@ import ( | |
) | ||
|
||
// baseDataset is a deployed database schema. | ||
// It implements the Dataset interface. | ||
// It implements the precompiles.Instance interface. | ||
type baseDataset struct { | ||
// schema is the schema of the dataset. | ||
schema *common.Schema | ||
|
@@ -23,9 +23,11 @@ type baseDataset struct { | |
global *GlobalContext | ||
} | ||
|
||
var _ precompiles.Instance = (*baseDataset)(nil) | ||
|
||
// Call calls a procedure from the dataset. | ||
// If the procedure is not public, it will return an error. | ||
// It implements the Namespace interface. | ||
// It satisfies precompiles.Instance. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think these interfaces have been renamed a few times. This seems to be the one it should mention. |
||
func (d *baseDataset) Call(caller *precompiles.ProcedureContext, app *common.App, method string, inputs []any) ([]any, error) { | ||
proc, ok := d.procedures[method] | ||
if !ok { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,10 +19,28 @@ import ( | |
"github.com/kwilteam/kwil-db/parse/sql/tree" | ||
) | ||
|
||
// MaxStackDepth is the limit on the number of nested procedure calls allowed. | ||
// This is different from the Go call stack depth, which may be much higher as | ||
// it depends on the program design. The value 1,000 was empirically selected to | ||
// be a call stack size of about 1MB and to provide a very high limit that no | ||
// reasonable schema would exceed (even 100 would suggest a poorly designed | ||
// schema). | ||
// | ||
// In addition to exorbitant memory required to support a call stack 1 million | ||
// deep (>1GB), the execution of that many calls can take seconds, even if they | ||
// do nothing else. | ||
// | ||
// Progressive gas metering may be used in the future to limit resources used by | ||
// abusive recursive calls, but a hard upper limit will likely be necessary | ||
// unless the price of an action call is extremely expensive or rises | ||
// exponentially at each level of the call stack. | ||
const MaxStackDepth = 1000 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can do up to 10,000, but that sort of txn requires about 0.1sec to exec even with no actual queries and that is too much baseline IMO. We sorta do need to spend each time an action is entered though, to make gas reflect DB utilization, but we can't just hard crash either. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think 1000 is certainly more than enough |
||
|
||
var ( | ||
ErrIncorrectNumberOfArguments = fmt.Errorf("incorrect number of arguments") | ||
ErrPrivateProcedure = fmt.Errorf("procedure is private") | ||
ErrMutativeProcedure = fmt.Errorf("procedure is mutative") | ||
ErrIncorrectNumberOfArguments = errors.New("incorrect number of arguments") | ||
ErrPrivateProcedure = errors.New("procedure is private") | ||
ErrMutativeProcedure = errors.New("procedure is mutative") | ||
ErrMaxStackDepth = errors.New("max call stack depth reached") | ||
) | ||
|
||
// instruction is an instruction that can be executed. | ||
|
@@ -277,9 +295,21 @@ var _ instructionFunc = (&callMethod{}).execute | |
// If no namespace is specified, the local namespace is used. | ||
// It will pass all arguments to the method, and assign the return values to the receivers. | ||
func (e *callMethod) execute(scope *precompiles.ProcedureContext, global *GlobalContext, db sql.DB) error { | ||
// This instruction is about to call into another procedure in this dataset | ||
// or another baseDataset. Check current call stack depth first. | ||
if scope.StackDepth >= MaxStackDepth { | ||
// NOTE: the actual Go call stack depth can be much more (e.g. more than | ||
// double) the procedure call depth depending on program design and the | ||
// number of Go function calls for each procedure. As of writing, it is | ||
// approximately double plus a handful from the caller: | ||
// | ||
// var pcs [4096]uintptr; fmt.Println("call stack depth", runtime.Callers(0, pcs[:])) | ||
return ErrMaxStackDepth | ||
} | ||
|
||
dataset, ok := global.datasets[scope.DBID] | ||
if !ok { | ||
return fmt.Errorf(`dataset "%s" not found`, scope.DBID) | ||
return fmt.Errorf("%w: %s", ErrDatasetNotFound, scope.DBID) | ||
} | ||
|
||
// getting these types to match the type required by the the ultimate DML | ||
|
@@ -304,6 +334,7 @@ func (e *callMethod) execute(scope *precompiles.ProcedureContext, global *Global | |
var err error | ||
|
||
newScope := scope.NewScope() | ||
newScope.StackDepth++ // not done by NewScope since (*baseDataset).Call would do it again | ||
|
||
// if no namespace is specified, we call a local procedure. | ||
// this can access public and private procedures. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initially I had this with a
+1
from the parent procedure's context, but(*baseDataset).Call
also doesNewScope
so it can get incremented twice unintentionally.