Skip to content

Multi-context execution (py.Context) #144

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

Closed
wants to merge 81 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
fc66b7f
py.CompileMode now a string enum
Dec 8, 2021
d10c35f
addressed benign Go warnings
Dec 8, 2021
2161b71
make Float implement M__index__
Dec 8, 2021
247ec42
added string helper functions
Dec 8, 2021
9a75271
updated to 1.15
Dec 8, 2021
90a8988
new Ctx-aware module model
Dec 9, 2021
c51644e
py.Ctx model allows concurrent execution
Dec 9, 2021
7f7d213
repl.New() now accepts an existing py.Ctx
Dec 9, 2021
708bb7e
fixed typo in callInternal()
Dec 9, 2021
a244256
unbroke test compatibilty
Dec 9, 2021
75d8743
cleaned up py.Ctx initialization wrt sys and DefaultCtxOpts()
Dec 10, 2021
d5fb8e8
added multi Ctx benchmark
Dec 10, 2021
269bdc0
WIP
Dec 10, 2021
4db9c65
fixed py.NewCtx not being set
Dec 10, 2021
53d05db
fixed List.sort() detection of instance invocation
Dec 10, 2021
7fd1c29
separated path resolution from path evaluation
Dec 11, 2021
8f3a104
removed cruft
Dec 18, 2021
5ebaf66
place ctx at top
Dec 18, 2021
d423190
err msg tweak
drew-512 Dec 18, 2021
b33c266
typo fix
drew-512 Dec 18, 2021
efca761
for loops we can all agree on
drew-512 Dec 18, 2021
1569475
removed outdated comments
Dec 18, 2021
a9c5f1f
Merge branch 'master' of https://github.com/drew-512/gpython
Dec 18, 2021
6372aee
added info for Frame.Ctx
Dec 19, 2021
4af4f90
removed unnecessary assignment
Dec 19, 2021
aec59f8
copyright and formatting
Dec 19, 2021
74f1a19
Ctx and NewCtx docs
Dec 19, 2021
8fa6622
switch cleanup
Dec 19, 2021
26fb9aa
leaner ModuleImpl schema
Dec 20, 2021
8614aeb
added GetDict() impl
Dec 20, 2021
86531c6
Compile() now returns py.Code (vs py.Object)
Dec 20, 2021
f71b961
tighter py.Ctx invocation patterns
Dec 20, 2021
77a7954
WIP
Dec 20, 2021
6bba7da
reverted to 644
Dec 20, 2021
46d1052
chmod 644
Dec 20, 2021
66fad23
enhanced RunFile() to enclose RunInNewModule()
Dec 27, 2021
95afd7b
Update examples/multi-ctx/main.go
drew-512 Jan 10, 2022
319edb7
Update modules/runtime.go
drew-512 Jan 10, 2022
06a2eae
Update modules/runtime.go
drew-512 Jan 10, 2022
8d4d586
Update modules/runtime.go
drew-512 Jan 10, 2022
d475e67
Update vm/eval.go
drew-512 Jan 10, 2022
6afac99
Update py/run.go
drew-512 Jan 10, 2022
931c346
Update modules/runtime.go
drew-512 Jan 10, 2022
a7ce4bb
Update modules/runtime.go
drew-512 Jan 10, 2022
c1f8809
Update modules/runtime.go
drew-512 Jan 10, 2022
9f02cf1
Update modules/runtime.go
drew-512 Jan 10, 2022
85e3a4a
Update modules/runtime.go
drew-512 Jan 10, 2022
fbec34e
Update modules/runtime.go
drew-512 Jan 10, 2022
dbd54d8
code cleanuo
Jan 10, 2022
71a5604
Module doc str
Jan 10, 2022
634823c
consistent arg order of NewFunction
Jan 10, 2022
6a0c11e
license and formatting
Jan 10, 2022
6242836
comment and cruft cleanup
Jan 10, 2022
61c7830
renamed Store to ModuleStore + docs
Jan 10, 2022
bb98aa8
added import docs
Jan 10, 2022
36b1018
shadowing cleanup
Jan 10, 2022
f2602bc
docs tweaks
Jan 10, 2022
e25e3bb
code cleanup
Jan 10, 2022
9421b6b
synced with gpython
Jan 10, 2022
92aed8a
Merge branch 'go-python:master' into master
drew-512 Jan 10, 2022
96515ea
Update modules/runtime.go
drew-512 Jan 10, 2022
f62f2b7
Update modules/runtime.go
drew-512 Jan 10, 2022
3bdba62
reverted from %w to %v (linux CI compile failure)
Jan 10, 2022
8033615
renamed py.Ctx to py.Context
Jan 10, 2022
681abf2
added Context.Close() and ModuleImpl.OnContextClosed()
Jan 18, 2022
7508556
docs and LoadIntsFromList
Jan 19, 2022
99bcb40
rename edits
Jan 21, 2022
0cc7650
push/popBusy now private
Jan 21, 2022
9afb2d3
doc edits
Jan 21, 2022
7d2a8d9
fixed kwarg issues in ParseTupleAndKeywords() and builtin_print test
Jan 25, 2022
ba7db80
added Flush kwarg to print test
Jan 25, 2022
61e42c8
added embedding example
Jan 26, 2022
168c337
main README makeover
Jan 26, 2022
2b7e1ab
fixed type conversion
Jan 26, 2022
3821b4b
Update README.md
drew-512 Jan 28, 2022
170f0d2
Update README.md
drew-512 Jan 28, 2022
b68b9f6
Update py/util.go
drew-512 Jan 28, 2022
759023b
Update py/util.go
drew-512 Jan 28, 2022
0c77716
Update py/util.go
drew-512 Jan 28, 2022
505755d
LoadIntsFromList cleanup
Jan 28, 2022
02d6b3e
comment edits
Jan 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
added Context.Close() and ModuleImpl.OnContextClosed()
  • Loading branch information
Drew O'Meara committed Jan 18, 2022
commit 681abf2de628248df3cbba21d4e29b80e21e2232
67 changes: 61 additions & 6 deletions modules/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"path"
"path/filepath"
"strings"
"sync"

"github.com/go-python/gpython/marshal"
"github.com/go-python/gpython/py"
Expand All @@ -29,14 +30,22 @@ func init() {

// context implements py.Context
type context struct {
store *py.ModuleStore
opts py.ContextOpts
store *py.ModuleStore
opts py.ContextOpts
closeOnce sync.Once
closing bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is closing that useful?
it doesn't seem to be really used as of yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent is that Close() can be use reliably and easily, so multiple concurrent Close() support seems important. And when it comes at no allocation cost, that makes sense to me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not disputing the fact that Close() is useful, but AFAICT closing is accessed twice (once initialized with false, then modified to true) but its value isn't used much otherwise.

closed bool
running sync.WaitGroup
done chan struct{}
}

// See py.Context interface
func NewContext(opts py.ContextOpts) py.Context {
ctx := &context{
opts: opts,
opts: opts,
done: make(chan struct{}),
closing: false,
closed: false,
}

ctx.store = py.NewModuleStore()
Expand All @@ -51,7 +60,11 @@ func NewContext(opts py.ContextOpts) py.Context {
}

func (ctx *context) ModuleInit(impl *py.ModuleImpl) (*py.Module, error) {
var err error
err := ctx.PushBusy()
defer ctx.PopBusy()
if err != nil {
return nil, err
}

if impl.Code == nil && len(impl.CodeSrc) > 0 {
impl.Code, err = py.Compile(string(impl.CodeSrc), impl.Info.FileDesc, py.ExecMode, 0, true)
Expand All @@ -72,7 +85,7 @@ func (ctx *context) ModuleInit(impl *py.ModuleImpl) (*py.Module, error) {
}
}

module, err := ctx.Store().NewModule(ctx, impl.Info, impl.Methods, impl.Globals)
module, err := ctx.Store().NewModule(ctx, impl)
if err != nil {
return nil, err
}
Expand All @@ -88,14 +101,20 @@ func (ctx *context) ModuleInit(impl *py.ModuleImpl) (*py.Module, error) {
}

func (ctx *context) ResolveAndCompile(pathname string, opts py.CompileOpts) (py.CompileOut, error) {
err := ctx.PushBusy()
defer ctx.PopBusy()
if err != nil {
return py.CompileOut{}, err
}

tryPaths := defaultPaths
if opts.UseSysPaths {
tryPaths = ctx.Store().MustGetModule("sys").Globals["path"].(*py.List).Items
}

out := py.CompileOut{}

err := resolveRunPath(pathname, opts, tryPaths, func(fpath string) (bool, error) {
err = resolveRunPath(pathname, opts, tryPaths, func(fpath string) (bool, error) {

stat, err := os.Stat(fpath)
if err == nil && stat.IsDir() {
Expand Down Expand Up @@ -162,6 +181,36 @@ func (ctx *context) ResolveAndCompile(pathname string, opts py.CompileOpts) (py.
return out, nil
}

func (ctx *context) PushBusy() error {
if ctx.closed {
return py.ExceptionNewf(py.RuntimeError, "Context closed")
}
ctx.running.Add(1)
return nil
}

func (ctx *context) PopBusy() {
ctx.running.Done()
}

// Close -- see type py.Context
func (ctx *context) Close() {
ctx.closeOnce.Do(func() {
ctx.closing = true
ctx.running.Wait()
ctx.closed = true

// Give each module a chance to release resources
ctx.store.OnContextClosed()
close(ctx.done)
})
}

// Done -- see type py.Context
func (ctx *context) Done() <-chan struct{} {
return ctx.done
}

var defaultPaths = []py.Object{
py.String("."),
}
Expand Down Expand Up @@ -216,6 +265,12 @@ func resolveRunPath(runPath string, opts py.CompileOpts, pathObjs []py.Object, t
}

func (ctx *context) RunCode(code *py.Code, globals, locals py.StringDict, closure py.Tuple) (py.Object, error) {
err := ctx.PushBusy()
defer ctx.PopBusy()
if err != nil {
return nil, err
}

return vm.EvalCode(ctx, code, globals, locals, nil, nil, nil, nil, closure)
}

Expand Down
87 changes: 51 additions & 36 deletions py/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import (
type ModuleFlags int32

const (
// Set for modules that are threadsafe, stateless, and/or can be shared across multiple py.Context instances (for efficiency).
// Otherwise, a separate module instance is created for each py.Context that imports it.
ShareModule ModuleFlags = 0x01 // @@TODO
// ShareModule signals that an embedded module is threadsafe and read-only, meaninging it could be shared across multiple py.Context instances (for efficiency).
// Otherwise, ModuleImpl will create a separate py.Module instance for each py.Context that imports it.
// This should be used with extreme caution since any module mutation (write) means possible cross-context data corruption.
ShareModule ModuleFlags = 0x01

MainModuleName = "__main__"
)
Expand All @@ -34,12 +35,13 @@ type ModuleInfo struct {
// By convention, .Code is executed when a module instance is initialized.
// If .Code == nil, then .CodeBuf or .CodeSrc will be auto-compiled to set .Code.
type ModuleImpl struct {
Info ModuleInfo
Methods []*Method
Globals StringDict
CodeSrc string // Module code body (py source code to be compiled)
CodeBuf []byte // Module code body (serialized py.Code object)
Code *Code // Module code body
Info ModuleInfo
Methods []*Method // Module-bound global method functions
Globals StringDict // Module-bound global variables
CodeSrc string // Module code body (source code to be compiled)
CodeBuf []byte // Module code body (serialized py.Code object)
Code *Code // Module code body
OnContextClosed func(*Module) // Callback for when a py.Context is closing to release resources
}

// ModuleStore is a container of Module imported into an owning py.Context.
Expand Down Expand Up @@ -87,10 +89,9 @@ func NewModuleStore() *ModuleStore {

// Module is a runtime instance of a ModuleImpl bound to the py.Context that imported it.
type Module struct {
ModuleInfo

Globals StringDict
Context Context
ModuleImpl *ModuleImpl // Parent implementation of this Module instance
Globals StringDict // Initialized from ModuleImpl.Globals
Context Context // Parent context that "owns" this Module instance
}

var ModuleType = NewType("module", "module object")
Expand All @@ -101,51 +102,68 @@ func (o *Module) Type() *Type {
}

func (m *Module) M__repr__() (Object, error) {
return String(fmt.Sprintf("<module %s>", m.Name)), nil
name, ok := m.Globals["__name__"].(String)
if !ok {
name = "???"
}
return String(fmt.Sprintf("<module %s>", string(name))), nil
}

// Get the Dict
func (m *Module) GetDict() StringDict {
return m.Globals
}

// Calls a named method of a module
func (m *Module) Call(name string, args Tuple, kwargs StringDict) (Object, error) {
attr, err := GetAttrString(m, name)
if err != nil {
return nil, err
}
return Call(attr, args, kwargs)
}

// Interfaces
var _ IGetDict = (*Module)(nil)

// NewModule adds a new Module instance to this ModuleStore.
// Each given Method prototype is used to create a new "live" Method bound this the newly created Module.
// This func also sets appropriate module global attribs based on the given ModuleInfo (e.g. __name__).
func (store *ModuleStore) NewModule(ctx Context, info ModuleInfo, methods []*Method, globals StringDict) (*Module, error) {
if info.Name == "" {
info.Name = MainModuleName
func (store *ModuleStore) NewModule(ctx Context, impl *ModuleImpl) (*Module, error) {
name := impl.Info.Name
if name == "" {
name = MainModuleName
}
m := &Module{
ModuleInfo: info,
Globals: globals.Copy(),
ModuleImpl: impl,
Globals: impl.Globals.Copy(),
Context: ctx,
}
// Insert the methods into the module dictionary
// Copy each method an insert each "live" with a ptr back to the module (which can also lead us to the host Context)
for _, method := range methods {
for _, method := range impl.Methods {
methodInst := new(Method)
*methodInst = *method
methodInst.Module = m
m.Globals[method.Name] = methodInst
}
// Set some module globals
m.Globals["__name__"] = String(info.Name)
m.Globals["__doc__"] = String(info.Doc)
m.Globals["__name__"] = String(name)
m.Globals["__doc__"] = String(impl.Info.Doc)
m.Globals["__package__"] = None
if len(info.FileDesc) > 0 {
m.Globals["__file__"] = String(info.FileDesc)
if len(impl.Info.FileDesc) > 0 {
m.Globals["__file__"] = String(impl.Info.FileDesc)
}
// Register the module
store.modules[info.Name] = m
store.modules[name] = m
// Make a note of some modules
switch info.Name {
switch name {
case "builtins":
store.Builtins = m
case "importlib":
store.Importlib = m
}
// fmt.Printf("Registering module %q\n", name)
// fmt.Printf("Registered module %q\n", moduleName)
return m, nil
}

Expand All @@ -167,14 +185,11 @@ func (store *ModuleStore) MustGetModule(name string) *Module {
return m
}

// Calls a named method of a module
func (m *Module) Call(name string, args Tuple, kwargs StringDict) (Object, error) {
attr, err := GetAttrString(m, name)
if err != nil {
return nil, err
// OnContextClosed signals all module instances that the parent py.Context has closed
func (store *ModuleStore) OnContextClosed() {
for _, m := range store.modules {
if m.ModuleImpl.OnContextClosed != nil {
m.ModuleImpl.OnContextClosed(m)
}
}
return Call(attr, args, kwargs)
}

// Interfaces
var _ IGetDict = (*Module)(nil)
56 changes: 42 additions & 14 deletions py/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,18 @@ type Context interface {
// RunCode is a lower-level invocation to execute the given py.Code.
RunCode(code *Code, globals, locals StringDict, closure Tuple) (result Object, err error)

// Execution of any of the above will stop when the next opcode runs
// @@TODO
// SignalHalt()

// Returns the named module for this context (or an error if not found)
GetModule(moduleName string) (*Module, error)

// Gereric access to this context's modules / state.
Store() *ModuleStore

// Close signals that is context is about to go out of scope and any internal resources should be released.
// Operations on a py.Context that have closed will generally result in an error.
Close()

// Done returns a signal that can be used to detect when this Context has fully closed / completed.
Done() <-chan struct{}
}

// CompileOpts specifies options for high-level compilation.
Expand Down Expand Up @@ -100,20 +103,45 @@ var (
Compile func(src, srcDesc string, mode CompileMode, flags int, dont_inherit bool) (*Code, error)
)

// RunFile resolves the given pathname, compiles as needed, and runs that code in the given module, returning the Module to indicate success.
// If inModule is a *Module, then the code is run in that module.
// If inModule is nil, the code is run in a new __main__ module (and the new Module is returned).
// If inModule is a string, the code is run in a new module with the given name (and the new Module is returned).
// RunFile resolves the given pathname, compiles as needed, executes the code in the given module, and returns the Module to indicate success.
//
// See RunCode() for description of inModule.
func RunFile(ctx Context, pathname string, opts CompileOpts, inModule interface{}) (*Module, error) {
out, err := ctx.ResolveAndCompile(pathname, opts)
if err != nil {
return nil, err
}

var moduleName string
createNew := false
var module *Module
return RunCode(ctx, out.Code, out.FileDesc, inModule)
}

// RunSrc compiles the given python buffer and executes it within the given module and returns the Module to indicate success.
//
// See RunCode() for description of inModule.
func RunSrc(ctx Context, pySrc string, pySrcDesc string, inModule interface{}) (*Module, error) {
if pySrcDesc == "" {
pySrcDesc = "<run>"
}
code, err := Compile(pySrc+"\n", pySrcDesc, SingleMode, 0, true)
if err != nil {
return nil, err
}

return RunCode(ctx, code, pySrcDesc, inModule)
}

// RunCode executes the given code object within the given module and returns the Module to indicate success.
// If inModule is a *Module, then the code is run in that module.
// If inModule is nil, the code is run in a new __main__ module (and the new Module is returned).
// If inModule is a string, the code is run in a new module with the given name (and the new Module is returned).
func RunCode(ctx Context, code *Code, codeDesc string, inModule interface{}) (*Module, error) {
var (
module *Module
moduleName string
err error
)

createNew := false
switch mod := inModule.(type) {

case string:
Expand All @@ -122,7 +150,7 @@ func RunFile(ctx Context, pathname string, opts CompileOpts, inModule interface{
case nil:
createNew = true
case *Module:
_, err = ctx.RunCode(out.Code, mod.Globals, mod.Globals, nil)
_, err = ctx.RunCode(code, mod.Globals, mod.Globals, nil)
module = mod
default:
err = ExceptionNewf(TypeError, "unsupported module type: %v", inModule)
Expand All @@ -132,9 +160,9 @@ func RunFile(ctx Context, pathname string, opts CompileOpts, inModule interface{
moduleImpl := ModuleImpl{
Info: ModuleInfo{
Name: moduleName,
FileDesc: out.FileDesc,
FileDesc: codeDesc,
},
Code: out.Code,
Code: code,
}
module, err = ctx.ModuleInit(&moduleImpl)
}
Expand Down
8 changes: 4 additions & 4 deletions py/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import (
"strconv"
)

var (
ErrUnsupportedObjType = errors.New("unsupported obj type")
)

func GetLen(obj Object) (Int, error) {
getlen, ok := obj.(I__len__)
if !ok {
Expand Down Expand Up @@ -53,10 +57,6 @@ func LoadTuple(args Tuple, vars []interface{}) error {
return nil
}

var (
ErrUnsupportedObjType = errors.New("unsupported obj type")
)

func LoadAttr(obj Object, attrName string, data interface{}) error {
attr, err := GetAttrString(obj, attrName)
if err != nil {
Expand Down
Loading