8000 SDK: add `moduleTypeDefs` to module SDK, to register type definition by eunomie · Pull Request #10584 · dagger/dagger · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

SDK: add moduleTypeDefs to module SDK, to register type definition #10584

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

eunomie
Copy link
Member
@eunomie eunomie commented Jun 13, 2025

Note

This has been extracted and cleaned from #10475
This is a first step to #10336 and #8030

This PR introduces a new moduleTypeDefs method modules can implement. This method acts like moduleRuntime and should returned a container. Currently the container returned by moduleRuntime is called in two different ways:

  • with an empty string for parent name: the engine is asking for the type definition of the module
  • with a parent name: the engine is invoking a function defined in the module

In Go for instance, this is done in the generated invoke functions with a code like the following:

switch parentName {
        case "MyModule":
                // invoking module's functions
        case "":
                // code that generates the type definition
                return dag.Module()....
}

The idea of moduleTypeDefs is to split those two responsibilities.:

  • when the engine used to call the runtime container with an empty parent name, the engine will execute the container from moduleTypeDefs
  • when the engine used to call the runtime container with a parent name, in order to invoke a module's functions, the container from moduleRuntime will be executed

Note

For compatibility reasons and the time to transition all modules to this new behaviour, if the moduleTypeDefs function is not exposed by the module SDK, then the moduleRuntime will be run and the container executed as before.

Go SDK

To simplify the management of those cases, the generated code responsible for the type registration (what was inside the case "") has been extracted into a specific register method.
The behaviour of the container returned by moduleRuntime is the same.
But the generated code for the container returned by moduleTypeDefs only contains the register function, not the invoke function. This allows to decouple the type definition from their invocation and will help to build self invocation without circular dependency issues.

Other SDKs

Other SDKs will follow in different pull requests to keep them easier to review. The Java one is almost ready and will follow.

Next

With the type definition decoupled from the invocation, it will be possible to inject the module types in input of the module to generate types against the module itself.

Comment on lines +202 to +204
TypeDefs(
context.Context,

// Current module dependencies.
*ModDeps,

// Current instance of the module source.
dagql.Instance[*ModuleSource],
) (*Container, error)
Copy link
Member

Choose a reason for hiding this comment

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

Should we not attempt to try and have this directly return a list of TypeDefs, or even a Module?

Even if the SDK runtime chooses to use a container behind the scenes, I think it makes sense to allow an SDK to directly return them if possible.

Copy link
Member Author

Choose a reason for hiding this comment

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

This was with the idea to keep the changes as small as possible, but yes it might make more sense to do that. I'll have a look.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm trying to see what would be the best option to do that, looking at both the Go SDK and the Java as they are quite different.

So I wonder if it wouldn't be a good idea to add something like a fromJSON or load function on the dagger.Module type that will take a json representation of those types.
So we can dag.Module().FromJSON(jsonTypesFile) and the runtime code is very small and almost shareable between SDKs, no function execution required as currently.
In Java SDK that could be something like:

func (m *JavaSdk) ModuleTypeDefs(
	ctx context.Context,
	modSource *dagger.ModuleSource,
	introspectionJSON *dagger.File,
) (*dagger.Module, error) {
	mvnCtr, err := m.codegenBase(ctx, modSource, introspectionJSON)
	if err != nil {
		return nil, err
	}
	
	// returns a json file with the type details
	typesJson, err := m.generateTypes(ctx, mvnCtr, introspectionJSON)
	
	return dag.Module().
		FromJSON(typesJson), nil
}

The main change is the SDK needs to generate this json file instead of the type registration code.
Basically this is just this code but in JSON:

	return dag.Module().
		WithObject(
			dag.TypeDef().WithObject("MyModule", dagger.TypeDefWithObjectOpts{SourceMap: dag.SourceMap("main.go", 8, 6)}).
				WithFunction(
					dag.Function("ContainerEcho",
						dag.TypeDef().WithObject("Container")).
						WithSourceMap(dag.SourceMap("main.go", 10, 1)).
						WithArg("stringArg", dag.TypeDef().WithKind(dagger.TypeDefKindStringKind), dagger.FunctionWithArgOpts{SourceMap: dag.SourceMap("main.go", 10, 34)})).
				WithFunction(
					dag.Function("Hello",
						dag.TypeDef().WithKind(dagger.TypeDefKindStringKind)).
						WithSourceMap(dag.SourceMap("main.go", 14, 1))))

@jedevc is that the kind of thing you had in mind?

Copy link
Member

Choose a reason for hiding this comment

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

Yes! This is pretty much exactly what I had in mind as the end state. To simplify getting to that state, we could even have dag.Module().AsJSON(), so we keep similar typedef code for now and transfer it later? Just thinking out loud.

But yeah, in the spirit of keeping changes minimal for now, we only need to change the TypeDefs interface function to return a Module - under the hood, we can still do the changes in this patch, where we're dispatching that to a Container.

We can come and do the big JSON changes for each SDK one-by-one later, as a performance optimization.

@eunomie eunomie force-pushed the moduleTypeDefs branch 3 times, most recently from 2679ef5 to 479970e Compare June 19, 2025 13:39
eunomie added 3 commits June 19, 2025 17:12
Go SDK now has a moduleTypeDefs method that is dedicated to return
types exposed by the module.

The `moduleTypeDefs` is similar to `moduleRuntime` but the generated
entrypoint only contains the types and not the invocation code.

Codegen contains a new flag `--typedefs-only` to only generate the
part related to types.

Other than this flag, one change in the codegen is a `register` function
is created to handle the management of the types. `invoke` function
will call `register` when the parent name is empty.
And basically when `--typedefs-only` is set, the invoke function is not
created but the `register` is and is called instead of `invoke`.

Signed-off-by: Yves Brissaud <gh@lgtd.io>
This is an optional new entry point on the module SDK.
It's designed to perform the type definition from the module like
the actual moduleRuntime when an empty string is passed as argument.

If the function doesn't exist, the moduleRuntime is used instead.

But this allows to decouple both of them so we can registry types
of a module without the runnable module itself because to build it
needs the types defined by the module

Signed-off-by: Yves Brissaud <gh@lgtd.io>
Use `moduleTypeDefs` instead of `moduleRuntime` if available.
Cache the runtime as expected if `moduleTypeDefs` has been called.

Signed-off-by: Yves Brissaud <gh@lgtd.io>
@eunomie eunomie force-pushed the moduleTypeDefs branch 2 times, most recently from 3fb3896 to f7d341d Compare June 20, 2025 15:53
Signed-off-by: Yves Brissaud <gh@lgtd.io>
@eunomie eunomie marked this pull request as ready for review June 20, 2025 16:44
@sipsma
Copy link
Contributor
sipsma commented Jun 20, 2025

@eunomie Looked at the socket not found error getting hit. It has to do with the fact that the new TypeDefs container and Runtime container are different (due to the new --typedefs-only arg) and thus end up as different cache entries. The Runtime container now doesn't get evaluated until the actual function invocation occurs, at which time it's running codegen (again) and looking for the SSH socket that's not available at that time.

I think the fundamental problem is that we end up running codegen twice now in this new scheme. That's not ideal for performance besides the socket issue it introduces.

I think splitting out the new TypeDefs method in the SDK interface makes perfect sense. But at least for this first step maybe it would make more sense to just implement TypeDefs for the Go SDK using the existing Runtime implementation? So TypeDefs for the go sdk just calls to Runtime and uses that container rather than trying to pass that extra --typedefs-only arg and bust cache between the two.

I haven't parsed through the exact behavior changes --typedefs-only introduces, so let me know if the suggestion above isn't actually possible. I'm just going off the idea the previously we used the Runtime container for getting typedefs, so it should theoretically be possible to continue to use it for now to implement TypeDefs.

@eunomie
Copy link
Member Author
eunomie commented Jun 21, 2025

Thanks @sipsma for the explanation.

I think the fundamental problem is that we end up running codegen twice now in this new scheme. That's not ideal for performance besides the socket issue it introduces.

I think splitting out the new TypeDefs method in the SDK interface makes perfect sense. But at least for this first step maybe it would make more sense to just implement TypeDefs for the Go SDK using the existing Runtime implementation? So TypeDefs for the go sdk just calls to Runtime and uses that container rather than trying to pass that extra --typedefs-only arg and bust cache between the two.

The goal is to run codegen twice, and to decouple typedefs from runtime.
It's not this PR specifically, but the final goal is to allow self invocation. So this means to have the typedefs independently from the runtime, as the runtime might refer to the typedefs.

A very simple example would be the following:

func (m *MyModule) ContainerEcho(stringArg string) *dagger.Container {
	return dag.Container().From("alpine:latest").WithExec([]string{"echo", stringArg})
}

func (m *MyModule) Echo(stringArg string) *dagger.Container {
	return dag.MyModule().ContainerEcho(stringArg)
}

The moduleRuntime codegen will never succeed because of the dependency to MyModule. In fact we have sort of a circular dependency here. Currently typedefs depends on runtime, that requires codegen, that requires typedefs.
So my solution was to split it in two:

  • the typedefs only codegen, that only looks at the types exposed and not at the code called
  • the runtime that doesn't care about types but only about function calling

All that means 2 codegen phases, one for typedefs, one for function calls. Right now it's the same with just a flag, that could be completely different, or use something like @jedevc is mentioning above, but my understanding is it will not really change a lot to this problem, no?

So yes, for this very first phase it would be possible to avoid the different codegen and only use the existing moduleRuntime, but it means the very next phase is to split it. So not sure that would have a lot of benefit to push back this split. Or maybe there's a benefit to have the moduleTypeDefs relying on moduleRuntime that I don't see/understand.

@eunomie
Copy link
Member Author
eunomie commented Jun 21, 2025

Regarding performances, yes there's a cost here. But I don't see a lot of different ways to avoid it.
Maybe the above suggestion to completely abstract the types and avoid to return a container to run can help, at least for the Java SDK I can see that as a good solution. But there will be a cost.

Thinking again, maybe the idea of an intermediate json might help has there's not anymore this typedefs container. Maybe.

@eunomie
Copy link
Member Author
eunomie commented Jun 21, 2025

The Runtime container now doesn't get evaluated until the actual function invocation occurs, at which time it's running codegen (again) and looking for the SSH socket that's not available at that time.

Could you detail a bit more? I'm not sure to understand the link between the container cached and not evaluated and the SSH socket.

@jedevc
Copy link
Member
jedevc commented Jun 23, 2025

I haven't parsed through the exact behavior changes --typedefs-only introduces, so let me know if the suggestion above isn't actually possible. I'm just going off the idea the previously we used the Runtime container for getting typedefs, so it should theoretically be possible to continue to use it for now to implement TypeDefs.

Hah, okay, I chatted with @eunomie, lemme try and summarize and explain.

So, this is part of the work to do self-calls. We need the separation into two phases for this. For the go sdk (as currently structured in main) - with self calls, we won't actually be able do the typedef analysis until after we've done codegen.

Here's why:

  1. The engine will get all the dependencies and try to do codegen, by generating a client and types and methods, etc.
  2. Now, if the module code is actually valid, we can build it using go build into the /runtime.
  3. We can then call /runtime with the empty string to get all the typedefs.

But. In the scenario of self-calls the code won't be valid after generating all the types and methods - because we'll be missing the self-bindings (we only know what those types should be after step 3). So we can't build it, so we can't get the typedefs. We have this weird circular dependency.

Thankfully, we can fix this. TypeDefs doesn't need to be dependent on codegen, it's kinda artificial - we can do all the go introspection of function signatures, and generate the typedefs without knowing every single type in the signature. One option (I think currently implemented in this PR) is to generate a single standalone binary (that doesn't even include our own source code) that only generates the typedefs, then we can build this, and actually get all the types at the end.

So, we need these two phases. To make sure that we fix the caching issue that @sipsma points out, we need to eagerly evaluate the Runtime container when we load the module. Atm in main, this is implied - we could just make that explicit (e.g. in ModuleSource.AsModule, just call .Runtime and throw away the result, so that it populates the cache) and that should solve the bug.

However, as @sipsma points out, there's a little bit of a perf issue here (curious to see how this pans out though, it might not be terrible?). For every module load, we're now having to generate, build, and execute two binaries. Ideally, the solution to this is to improve TypeDefs so that we don't need to build a binary at all - instead of generating code that builds typedefs, we could just port that code (might be automatable using an LLM?) to just directly generate the typedef structure (e.g. as JSON, see the suggestion above). Functionally, this would mean changing the method TypeDefCode to TypeDefJSON or similar:

func (spec *parsedObjectType) TypeDefCode() (*Statement, error) {

@eunomie
Copy link
Member Author
eunomie commented Jun 24, 2025

This is not a solution at all, but I just confirmed that with this change, that works:

diff --git a/core/sdk/go_sdk.go b/core/sdk/go_sdk.go
index 3d6fc8390..664aff2e9 100644
--- a/core/sdk/go_sdk.go
+++ b/core/sdk/go_sdk.go
@@ -523,13 +523,13 @@ func (sdk *goSDK) baseWithCodegen(
 	// cause failures if we also mount this in Runtime.
 	// Issue we run into is that when we try to run sdk checks
 	// using .dagger, it fails trying to find the socket
-	sshAuthSelectors, err := sdk.getUnixSocketSelector(ctx)
-	if err != nil {
-		return ctr, err
-	}
-	if len(sshAuthSelectors) > 0 {
-		selectors = append(selectors, sshAuthSelectors...)
-	}
+	//sshAuthSelectors, err := sdk.getUnixSocketSelector(ctx)
+	//if err != nil {
+	//	return ctr, err
+	//}
+	//if len(sshAuthSelectors) > 0 {
+	//	selectors = append(selectors, sshAuthSelectors...)
+	//}
 
 	// now that we are done with gitconfig and injecting env
 	// variables, we can run the codegen command.

(if there's no need for a private repo)

My understanding is the first codegen part (the one generating the typedefs) shouldn't require the ssh socket, as it shouldn't need the dependencies if I'm right (functions can return a local type or a core type, but not a dependent type, right?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
0