Skip to content

Commit e9657d8

Browse files
authored
Add support for stable Func keys in App Engine second gen (#184)
Func keys include the filename where the Func is created. The filename is parsed according to these rules: * Paths in package main are shortened to just the file name (github.com/foo/foo.go -> foo.go) * Paths are stripped to just package paths (/go/src/github.com/foo/bar.go -> github.com/foo/bar.go) * Module versions are stripped (/go/pkg/mod/github.com/foo/[email protected]/baz.go -> github.com/foo/bar/baz.go)
1 parent a37df13 commit e9657d8

File tree

6 files changed

+185
-3
lines changed

6 files changed

+185
-3
lines changed

delay/delay.go

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,16 @@ be associated with the request that invoked the Call method.
3434
The state of a function invocation that has not yet successfully
3535
executed is preserved by combining the file name in which it is declared
3636
with the string key that was passed to the Func function. Updating an app
37-
with pending function invocations is safe as long as the relevant
38-
functions have the (filename, key) combination preserved.
37+
with pending function invocations should safe as long as the relevant
38+
functions have the (filename, key) combination preserved. The filename is
39+
parsed according to these rules:
40+
* Paths in package main are shortened to just the file name (github.com/foo/foo.go -> foo.go)
41+
* Paths are stripped to just package paths (/go/src/github.com/foo/bar.go -> github.com/foo/bar.go)
42+
* Module versions are stripped (/go/pkg/mod/github.com/foo/[email protected]/baz.go -> github.com/foo/bar/baz.go)
43+
44+
There is some inherent risk of pending function invocations being lost during
45+
an update that contains large changes. For example, switching from using GOPATH
46+
to go.mod is a large change that may inadvertently cause file paths to change.
3947
4048
The delay package uses the Task Queue API to create tasks that call the
4149
reserved application path "/_ah/queue/go/delay".
@@ -50,13 +58,19 @@ import (
5058
"encoding/gob"
5159
"errors"
5260
"fmt"
61+
"go/build"
62+
stdlog "log"
5363
"net/http"
64+
"path/filepath"
5465
"reflect"
66+
"regexp"
5567
"runtime"
68+
"strings"
5669

5770
"golang.org/x/net/context"
5871

5972
"google.golang.org/appengine"
73+
"google.golang.org/appengine/internal"
6074
"google.golang.org/appengine/log"
6175
"google.golang.org/appengine/taskqueue"
6276
)
@@ -98,6 +112,45 @@ func isContext(t reflect.Type) bool {
98112
return t == stdContextType || t == netContextType
99113
}
100114

115+
var modVersionPat = regexp.MustCompile("@v[^/]+")
116+
117+
// fileKey finds a stable representation of the caller's file path.
118+
// For calls from package main: strip all leading path entries, leaving just the filename.
119+
// For calls from anywhere else, strip $GOPATH/src, leaving just the package path and file path.
120+
func fileKey(file string) (string, error) {
121+
if !internal.IsSecondGen() || internal.MainPath == "" {
122+
return file, nil
123+
}
124+
// If the caller is in the same Dir as mainPath, then strip everything but the file name.
125+
if filepath.Dir(file) == internal.MainPath {
126+
return filepath.Base(file), nil
127+
}
128+
// If the path contains "_gopath/src/", which is what the builder uses for
129+
// apps which don't use go modules, strip everything up to and including src.
130+
// Or, if the path starts with /tmp/staging, then we're importing a package
131+
// from the app's module (and we must be using go modules), and we have a
132+
// path like /tmp/staging1234/srv/... so strip everything up to and
133+
// including the first /srv/.
134+
// And be sure to look at the GOPATH, for local development.
135+
s := string(filepath.Separator)
136+
for _, s := range []string{filepath.Join("_gopath", "src") + s, s + "srv" + s, filepath.Join(build.Default.GOPATH, "src") + s} {
137+
if idx := strings.Index(file, s); idx > 0 {
138+
return file[idx+len(s):], nil
139+
}
140+
}
141+
142+
// Finally, if that all fails then we must be using go modules, and the file is a module,
143+
// so the path looks like /go/pkg/mod/github.com/foo/[email protected]/baz.go
144+
// So... remove everything up to and including mod, plus the @.... version string.
145+
m := "/mod/"
146+
if idx := strings.Index(file, m); idx > 0 {
147+
file = file[idx+len(m):]
148+
} else {
149+
return file, fmt.Errorf("fileKey: unknown file path format for %q", file)
150+
}
151+
return modVersionPat.ReplaceAllString(file, ""), nil
152+
}
153+
101154
// Func declares a new Function. The second argument must be a function with a
102155
// first argument of type context.Context.
103156
// This function must be called at program initialization time. That means it
@@ -111,7 +164,12 @@ func Func(key string, i interface{}) *Function {
111164

112165
// Derive unique, somewhat stable key for this func.
113166
_, file, _, _ := runtime.Caller(1)
114-
f.key = file + ":" + key
167+
fk, err := fileKey(file)
168+
if err != nil {
169+
// Not fatal, but log the error
170+
stdlog.Printf("delay: %v", err)
171+
}
172+
f.key = fk + ":" + key
115173

116174
t := f.fv.Type()
117175
if t.Kind() != reflect.Func {

delay/delay_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"fmt"
1313
"net/http"
1414
"net/http/httptest"
15+
"os"
16+
"path/filepath"
1517
"reflect"
1618
"testing"
1719

@@ -462,3 +464,78 @@ func TestStandardContext(t *testing.T) {
462464
t.Errorf("stdCtxRuns: got %d, want 1", stdCtxRuns)
463465
}
464466
}
467+
468+
func TestFileKey(t *testing.T) {
469+
os.Setenv("GAE_ENV", "standard")
470+
tests := []struct {
471+
mainPath string
472+
file string
473+
want string
474+
}{
475+
// first-gen
476+
{
477+
"",
478+
filepath.FromSlash("srv/foo.go"),
479+
filepath.FromSlash("srv/foo.go"),
480+
},
481+
// gopath
482+
{
483+
filepath.FromSlash("/tmp/staging1234/srv/"),
484+
filepath.FromSlash("/tmp/staging1234/srv/foo.go"),
485+
"foo.go",
486+
},
487+
{
488+
filepath.FromSlash("/tmp/staging1234/srv/_gopath/src/example.com/foo"),
489+
filepath.FromSlash("/tmp/staging1234/srv/_gopath/src/example.com/foo/foo.go"),
490+
"foo.go",
491+
},
492+
{
493+
filepath.FromSlash("/tmp/staging2234/srv/_gopath/src/example.com/foo"),
494+
filepath.FromSlash("/tmp/staging2234/srv/_gopath/src/example.com/foo/bar/bar.go"),
495+
filepath.FromSlash("example.com/foo/bar/bar.go"),
496+
},
497+
{
498+
filepath.FromSlash("/tmp/staging3234/srv/_gopath/src/example.com/foo"),
499+
filepath.FromSlash("/tmp/staging3234/srv/_gopath/src/example.com/bar/main.go"),
500+
filepath.FromSlash("example.com/bar/main.go"),
501+
},
502+
// go mod, same package
503+
{
504+
filepath.FromSlash("/tmp/staging3234/srv"),
505+
filepath.FromSlash("/tmp/staging3234/srv/main.go"),
506+
"main.go",
507+
},
508+
{
509+
filepath.FromSlash("/tmp/staging3234/srv"),
510+
filepath.FromSlash("/tmp/staging3234/srv/bar/main.go"),
511+
filepath.FromSlash("bar/main.go"),
512+
},
513+
{
514+
filepath.FromSlash("/tmp/staging3234/srv/cmd"),
515+
filepath.FromSlash("/tmp/staging3234/srv/cmd/main.go"),
516+
"main.go",
517+
},
518+
{
519+
filepath.FromSlash("/tmp/staging3234/srv/cmd"),
520+
filepath.FromSlash("/tmp/staging3234/srv/bar/main.go"),
521+
filepath.FromSlash("bar/main.go"),
522+
},
523+
// go mod, other package
524+
{
525+
filepath.FromSlash("/tmp/staging3234/srv"),
526+
filepath.FromSlash("/go/pkg/mod/github.com/foo/[email protected]/baz.go"),
527+
filepath.FromSlash("github.com/foo/bar/baz.go"),
528+
},
529+
}
530+
for i, tc := range tests {
531+
internal.MainPath = tc.mainPath
532+
got, err := fileKey(tc.file)
533+
if err != nil {
534+
t.Errorf("Unexpected error, call %v, file %q: %v", i, tc.file, err)
535+
continue
536+
}
537+
if got != tc.want {
538+
t.Errorf("Call %v, file %q: got %q, want %q", i, tc.file, got, tc.want)
539+
}
540+
}
541+
}

internal/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ import (
1111
)
1212

1313
func Main() {
14+
MainPath = ""
1415
appengine_internal.Main()
1516
}

internal/main_common.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package internal
2+
3+
// MainPath stores the file path of the main package. On App Engine Standard
4+
// using Go version 1.9 and below, this will be unset. On App Engine Flex and
5+
// App Engine Standard second-gen (Go 1.11 and above), this will be the
6+
// filepath to package main.
7+
var MainPath string

internal/main_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// +build !appengine
2+
3+
package internal
4+
5+
import (
6+
"go/build"
7+
"path/filepath"
8+
"testing"
9+
)
10+
11+
func TestFindMainPath(t *testing.T) {
12+
// Tests won't have package main, instead they have testing.tRunner
13+
want := filepath.Join(build.Default.GOROOT, "src", "testing", "testing.go")
14+
got := findMainPath()
15+
if want != got {
16+
t.Errorf("findMainPath: want %s, got %s", want, got)
17+
}
18+
}

internal/main_vm.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ import (
1212
"net/http"
1313
"net/url"
1414
"os"
15+
"path/filepath"
16+
"runtime"
1517
)
1618

1719
func Main() {
20+
MainPath = filepath.Dir(findMainPath())
1821
installHealthChecker(http.DefaultServeMux)
1922

2023
port := "8080"
@@ -31,6 +34,24 @@ func Main() {
3134
}
3235
}
3336

37+
// Find the path to package main by looking at the root Caller.
38+
func findMainPath() string {
39+
pc := make([]uintptr, 100)
40+
n := runtime.Callers(2, pc)
41+
frames := runtime.CallersFrames(pc[:n])
42+
for {
43+
frame, more := frames.Next()
44+
// Tests won't have package main, instead they have testing.tRunner
45+
if frame.Function == "main.main" || frame.Function == "testing.tRunner" {
46+
return frame.File
47+
}
48+
if !more {
49+
break
50+
}
51+
}
52+
return ""
53+
}
54+
3455
func installHealthChecker(mux *http.ServeMux) {
3556
// If no health check handler has been installed by this point, add a trivial one.
3657
const healthPath = "/_ah/health"

0 commit comments

Comments
 (0)