#Issues with Changesets

1 messages · Page 1 of 1 (latest)

wraith bluff
#

Hey, I've been playing around with changesets to explore bootstrapping downstream repositories and have come across some issues. Happy to raise a issue for any.

  1. If I return a changeset, I get a interactive menu showing which files have changed and whether or not I want to apply the changes to the host. Is the intention to show a content diff or is it limited to simple file reporting? Not an issue, just curious if it is working as intended (image 1).

  2. Using --progress dots when a changeset is returned results in a malformed TUI (image 2).

#
  1. Using --progress report panics.
❯ dagger call bootstrap --src . --progress report
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x58 pc=0x105781c9c]
#
goroutine 1 [running]:
github.com/charmbracelet/bubbletea.(*Program).Send(...)
    /go/pkg/mod/github.com/charmbracelet/bubbletea@v1.3.10/tea.go:776
github.com/dagger/dagger/dagql/idtui.(*frontendPretty).HandleForm(0x140005d1508, {0x10635c038, 0x1400032c540}, 0x14000dce8c0)
    /app/dagql/idtui/frontend_pretty.go:378 +0xac
main.handleChangesetResponse({0x10635c038, 0x1400032c540}, 0x14000d353e0?, {0x105f82d20?, 0x1400058b7c0?}, 0x0)
    /app/cmd/dagger/functions.go:755 +0x474
main.handleResponse({0x10635c038, 0x1400032c540}, 0x1400039eab0, 0x14000d34660, {0x105f82d20, 0x1400058b7c0}, {0x14ecf3240, 0x140001901b0}, {0x14ecf3240, 0x140001901b8}, ...)
    /app/cmd/dagger/functions.go:662 +0x158
main.(*FuncCommand).makeSubCmd.(*FuncCommand).RunE.func2(0x14000db1808, {0x140005d0c08?, 0x140001fc780?, 0x5?})
    /app/cmd/dagger/functions.go:586 +0xf8
main.(*FuncCommand).execute(0x107af0e20, 0x140005d0c08, {0x140001fc780, 0x5, 0x5})
...
#
    /app/cmd/dagger/functions.go:316 +0x274
main.(*FuncCommand).Command.func2.1({0x10635c038, 0x1400032c540}, 0x14?)
    /app/cmd/dagger/functions.go:182 +0x168
main.(*FuncCommand).Command.func2.withEngine.2({0x10635c038, 0x14000272c30})
    /app/cmd/dagger/engine.go:145 +0x574
github.com/dagger/dagger/dagql/idtui.(*frontendPretty).Run(0x140005d1508, {0x10635c038?, 0x14000272c30?}, {0x0, 0x0, 0x1, 0x0, 0x0, 0x5f5e100, 0x3b9aca00, ...}, ...)
    /app/dagql/idtui/frontend_pretty.go:334 +0xd0
main.withEngine(...)
    /app/cmd/dagger/engine.go:72
main.(*FuncCommand).Command.func2(0x140005d0c08, {0x140001fc780, 0x5, 0x5})
    /app/cmd/dagger/functions.go:175 +0x284
github.com/spf13/cobra.(*Command).execute(0x140005d0c08, {0x140001fc780, 0x5, 0x5})
    /go/pkg/mod/github.com/spf13/cobra@v1.10.1/command.go:1015 +0x7d4
github.com/spf13/cobra.(*Command).ExecuteC(0x107afa260)
    /go/pkg/mod/github.com/spf13/cobra@v1.10.1/command.go:1148 +0x350
github.com/spf13/cobra.(*Command).Execute(...)
    /go/pkg/mod/github.com/spf13/cobra@v1.10.1/command.go:1071
github.com/spf13/cobra.(*Command).ExecuteContext(...)
    /go/pkg/mod/github.com/spf13/cobra@v1.10.1/command.go:1064
main.main()
    /app/cmd/dagger/main.go:475 +0x700
#

  1. If you attempt to add a variable with a value that already exists, the engine crashes with a stack overflow.
func (*Test) Bootstrap(
    ctx context.Context,
    // +ignore=["*", "!.env"]
    src *dagger.Directory,
) (*dagger.Changeset, error) {
    ok, err := src.Exists(ctx, ".env")
    if err != nil {
        return nil, fmt.Errorf("error checking for .env file: %w", err)
    }

    // No .env file, bootstrap one.
    if !ok {
        newEnv := dag.EnvFile().
            WithVariable("BOOTSTRAPPED", "TRUE").
            AsFile()

        return src.WithFile(".env", newEnv).Changes(src), nil
    }

    // .env file exists, update it.
    envFile := src.File(".env").
        AsEnvFile().
        WithVariable("UPDATED", "TRUE").
        AsFile()

    return src.WithFile(".env", envFile).Changes(src), nil
}

If .env already contains UPDATED=TRUE, it will crash.

#
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0x402698e330 stack=[0x402698e000, 0x404698e000]
fatal error: stack overflow

runtime stack:
runtime.throw({0x2ed8d5a?, 0x4000064008?})
    /usr/lib/go/src/runtime/panic.go:1094 +0x34 fp=0x4000b25e80 sp=0x4000b25e50 pc=0x8db04
runtime.newstack()
    /usr/lib/go/src/runtime/stack.go:1159 +0x44c fp=0x4000b25fb0 sp=0x4000b25e80 pc=0x728cc
runtime.morestack()
    /usr/lib/go/src/runtime/asm_arm64.s:392 +0x70 fp=0x4000b25fb0 sp=0x4000b25fb0 pc=0x93da0
half prairie
#

cc @dull hemlock looking from the distance could this be an edge case in the envFileContentHashWrapper?

dull hemlock
# half prairie cc <@691818044368093268> looking from the distance could this be an edge case i...

my best guess is some sort of infinite recursion in the engine?

... lots of calls to LoadType here ....
github.com/dagger/dagger/dagql.(*Server).LoadType(0x4000bc5a70, {0x3683670, 0x40041d15e0}, 0x4005a0ec80)
    /app/dagql/server.go:656 +0x94 fp=0x404698d3e0 sp=0x404698d350 pc=0xe6c784
github.com/dagger/dagger/dagql.(*Server).LoadType(0x4000bc5a70, {0x3683670, 0x40041d15e0}, 0x4005a0eb00)
    /app/dagql/server.go:656 +0x94 fp=0x404698d470 sp=0x404698d3e0 pc=0xe6c784
github.com/dagger/dagger/dagql.(*Server).Load(0x4000bc5a70, {0x3683670?, 0x40041d15e0?}, 0x1a128c?)
    /app/dagql/server.go:643 +0x28 fp=0x404698d4a0 sp=0x404698d470 pc=0xe6c698
github.com/dagger/dagger/dagql.ID[...].Load(0x36f0480?, {0x3683670?, 0x40041d15e0}, 0x13177c?)
    /app/dagql/types.go:874 +0x60 fp=0x404698d630 sp=0x404698d4a0 pc=0x1a30b80
github.com/dagger/dagger/core/schema.WithFileArgs.Inputs({{0x4005132913, 0x4}, {0x4005a0eb00, 0x0}, {0x0, 0x0}, {0x0, 0x0}, {{0x0}, {0x0, ...}}}, ...)
    /app/core/schema/directory.go:682 +0xdc fp=0x404698d750 sp=0x404698d630 pc=0x1ff21ac
github.com/dagger/dagger/core/schema.(*WithFileArgs).Inputs(0x5d69ca0?, {0x3683670?, 0x40041d15e0?})
    <autogenerated>:1 +0x84 fp=0x404698d820 sp=0x404698d750 pc=0x21a5c94
github.com/dagger/dagger/core.InputsOf({0x3683670?, 0x40041d15e0?}, {0x2c62b80?, 0x400586f620?})
    /app/core/util.go:133 +0x5c fp=0x404698d850 sp=0x404698d820 pc=0x1a28f9c
github.com/dagger/dagger/core/schema.DagOpDirectory[...]({0x3683670, 0x40041d15e0}, 0x4000972120, 0x400122ec80, {{0x4005132913, 0x4}, {0x4005a0eb00, 0x0}, {0x0, 0x0}, ...}, ...)
    /app/core/schema/wrapper.go:329 +0x120 fp=0x404698d970 sp=0x404698d850 pc=0x215bc30
half prairie
#

that's why I was wondering if the way we were handling the EnvFile queries could be the reason this recursion is being triggered

dull hemlock
#

I just gave it a try, and wasn't able to reproduce it. If it's possible to come up with a repro that doesn't require copying in a directory from the host, that would make it easier to replicate.

half prairie
dull hemlock
#

excellent! That failed for me too.

#

when I changed it to a different value, it worked. Super weird.

half prairie
dull hemlock
dull hemlock
#

no worries! curiosity got the best of me 😅

dull hemlock
#

I went for a deep-dive, and discovered that we have some deduplication of calls that get stored under https://github.com/alexcb/dagger/blob/9f95cf7484c465095203499848723979a9891f77/dagql/call/callpbv1/call.proto#L14-L23

when we start with, for example, envFile := dag.File(".env", "SAME=VALUE").AsEnvFile() and envFile.WithVariable("SAME", "VALUE") it results in both operation (e.g. the envFile, and WithVariable("SAME", "VALUE")) having the same digest. This means we only get a single operation, e.g. WithVariable("SAME", "VALUE") end up in the callsByDigest map. It's parent then points to itself since it assumes both operations are identical.

We need to somehow have a unique digest for each operation -- so we can correctly track the parent in the dag; however at the same time, we need to know that both operations produce the same output (i.e. have the same digest), so, for example, envFile.WithExec and envFile.WithVariable("SAME", "VALUE").WithExec map to the same operation -- otherwise we break the caching of WithExec.

#

I was able to get slightly closer to where this cycle happens on the pointer level by adding:

func (id *ID) FromProto(dagPB *callpbv1.DAG) error {
    if id == nil {
        return fmt.Errorf("cannot decode into nil ID")
    }
    if err := id.decode(dagPB.RootDigest, dagPB.CallsByDigest, map[string]*ID{}); err != nil {
        return fmt.Errorf("failed to decode DAG: %w", err)
    }
    id.AssertNoPointerCycles()
    return nil
}

func (id *ID) AssertNoPointerCycles() {
    seen := map[string]bool{}
    for id != nil {
        s := fmt.Sprintf("%p", id)
        fmt.Printf("ACB got pointer %s\n", s)
        if _, found := seen[s]; found {
            panic(fmt.Sprintf("pointer %s already seen", s))
        }
        seen[s] = true
        id = id.receiver
    }
}

to dagql/call/id.go

dull hemlock
#

@pallid copper can I pick your brain on my above reasoning? Should we avoid calling WithObjectDigest(...), unless specific cases such as dealing with host directories, files, etc? EnvFile is particularly fussy since it maintains an escape hatch to the host for looking up envs whenever there's a key miss.

pallid copper
#

We need to somehow have a unique digest for each operation -- so we can correctly track the parent in the dag; however at the same time, we need to know that both operations produce the same output (i.e. have the same digest), so, for example, envFile.WithExec and envFile.WithVariable("SAME", "VALUE").WithExec map to the same operation -- otherwise we break the caching of WithExec.
That's exactly what I added support for in https://github.com/dagger/dagger/pull/11729, which I fortunately merged just yesterday 🙂

#

I'd try .WithContentDigest and see how it goes. I was mainly focused in that PR on fixing some known problems, so I'm curious to see if it also perfectly covers the case we're hitting here. If it doesn't work lemme know and can figure out some more adjustments

dull hemlock
#

well that's prefect timing. I'll rebase against it and see if it helps in this case..... I see envFileContentHashWrapper has been updated already 🙂

#

hah, well the rebase did the trick! nice work on fixing this bug.