ichiban / prolog

The only reasonable scripting engine for Go.
MIT License
564 stars 27 forks source link

Pass context to predicate custom implementation #280

Closed ccamel closed 1 year ago

ccamel commented 1 year ago

As far as I see, the current VM implementation does not allow transmitting the context to the custom implementation of a predicate:

p.Register2(engine.NewAtom("foo"), func(_ *engine.VM, url, status engine.Term, k engine.Cont, env *engine.Env) *engine.Promise {

  // I would like to have the context.Context here from the caller.
  // ...

  return k(env)
}

To pass request-scoped values, cancelation signals, and deadlines that are required by some API calls, it is necessary to have a way to pass the context. IMHO, there are a few options for how to do this: adding a ctx parameter (though this would be a breaking change), adding a field to the engine.Env, or binding a variable on the context (though it is unclear if this is a suitable solution).

Thanks!

ichiban commented 1 year ago

Hi, you can capture the context.Context passed in p.QueryContext() or p.QuerySolutionContext() with engine.Delay():

package main

import (
    "context"
    "errors"
    "flag"
    "fmt"
    "net/http"
    "time"

    "github.com/ichiban/prolog"
    "github.com/ichiban/prolog/engine"
)

func main() {
    var (
        u string
        t time.Duration
    )
    flag.StringVar(&u, "url", "http://httpbin.org/status/200", "URL")
    flag.DurationVar(&t, "timeout", 1*time.Second, "timeout")
    flag.Parse()

    p := prolog.New(nil, nil)
    p.Register2(engine.NewAtom("get_status"), getStatus)

    ctx, cancel := context.WithTimeout(context.Background(), t)
    defer cancel()

    var s struct {
        Status int
    }
    switch err := p.QuerySolutionContext(ctx, `get_status(?, Status).`, u).Scan(&s); err {
    case nil:
        fmt.Printf("status: %d\n", s.Status)
    case context.DeadlineExceeded:
        fmt.Printf("timeout\n")
    default:
        panic(err)
    }
}

func getStatus(vm *engine.VM, url, status engine.Term, k engine.Cont, env *engine.Env) *engine.Promise {
    u, ok := env.Resolve(url).(engine.Atom)
    if !ok {
        return engine.Error(errors.New("not an atom"))
    }

    req, err := http.NewRequest(http.MethodGet, u.String(), nil)
    if err != nil {
        return engine.Error(err)
    }

    return engine.Delay(func(ctx context.Context) *engine.Promise {
        req := req.WithContext(ctx)
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return engine.Error(err)
        }
        return engine.Unify(vm, status, engine.Integer(resp.StatusCode), k, env)
    })
}
$ time go run main.go -url http://httpbin.org/delay/10
timeout

real    0m1.396s
user    0m0.302s
sys     0m0.175s
ccamel commented 1 year ago

Oh, that's great! 😀 Well, problem solved. Thank you!