This is an automated email from the ASF dual-hosted git repository.
altay pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new c4945d0 [BEAM-3041] Support various pip based dependency options for
python sdk (#4286)
c4945d0 is described below
commit c4945d0d1366a8a5b177994aa169b90f9c762537
Author: Ahmet Altay <[email protected]>
AuthorDate: Tue Dec 19 16:38:00 2017 -0800
[BEAM-3041] Support various pip based dependency options for python sdk
(#4286)
---
sdks/python/container/boot.go | 57 ++++++++++++++---
sdks/python/container/piputil.go | 131 +++++++++++++++++++++++++++++++++++++++
2 files changed, 178 insertions(+), 10 deletions(-)
diff --git a/sdks/python/container/boot.go b/sdks/python/container/boot.go
index 31c8267..69f031b 100644
--- a/sdks/python/container/boot.go
+++ b/sdks/python/container/boot.go
@@ -20,13 +20,15 @@ package main
import (
"context"
"flag"
+ "fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/apache/beam/sdks/go/pkg/beam/artifact"
- pb "github.com/apache/beam/sdks/go/pkg/beam/model/pipeline_v1"
+ pbpipeline "github.com/apache/beam/sdks/go/pkg/beam/model/pipeline_v1"
+ pbjob "github.com/apache/beam/sdks/go/pkg/beam/model/jobmanagement_v1"
"github.com/apache/beam/sdks/go/pkg/beam/provision"
"github.com/apache/beam/sdks/go/pkg/beam/util/execx"
"github.com/apache/beam/sdks/go/pkg/beam/util/grpcx"
@@ -44,6 +46,15 @@ var (
semiPersistDir = flag.String("semi_persist_dir", "/tmp", "Local
semi-persistent directory (optional).")
)
+const (
+ sdkHarnessEntrypoint = "apache_beam.runners.worker.sdk_worker_main"
+ // Please keep these names in sync with setup dependency.py
+ workflowFile = "workflow.tar.gz"
+ requirementsFile = "requirements.txt"
+ sdkFile = "dataflow_python_sdk.tar"
+ extraPackagesFile = "extra_packages.txt"
+)
+
func main() {
flag.Parse()
if *id == "" {
@@ -81,36 +92,62 @@ func main() {
dir := filepath.Join(*semiPersistDir, "staged")
- _, err = artifact.Materialize(ctx, *artifactEndpoint, dir)
+ files, err := artifact.Materialize(ctx, *artifactEndpoint, dir)
if err != nil {
log.Fatalf("Failed to retrieve staged files: %v", err)
}
// TODO(herohde): the packages to install should be specified
explicitly. It
// would also be possible to install the SDK in the Dockerfile.
- if err := pipInstall(joinPaths(dir, "dataflow_python_sdk.tar[gcp]"));
err != nil {
- log.Fatalf("Failed to install SDK: %v", err)
+ if setupErr := installSetupPackages(files, dir); setupErr != nil {
+ log.Fatalf("Failed to install SDK: %v", setupErr)
}
// (3) Invoke python
os.Setenv("PIPELINE_OPTIONS", options)
os.Setenv("SEMI_PERSISTENT_DIRECTORY", *semiPersistDir)
- os.Setenv("LOGGING_API_SERVICE_DESCRIPTOR",
proto.MarshalTextString(&pb.ApiServiceDescriptor{Url: *loggingEndpoint}))
- os.Setenv("CONTROL_API_SERVICE_DESCRIPTOR",
proto.MarshalTextString(&pb.ApiServiceDescriptor{Url: *controlEndpoint}))
+ os.Setenv("LOGGING_API_SERVICE_DESCRIPTOR",
proto.MarshalTextString(&pbpipeline.ApiServiceDescriptor{Url:
*loggingEndpoint}))
+ os.Setenv("CONTROL_API_SERVICE_DESCRIPTOR",
proto.MarshalTextString(&pbpipeline.ApiServiceDescriptor{Url:
*controlEndpoint}))
args := []string{
"-m",
- "apache_beam.runners.worker.sdk_worker_main",
+ sdkHarnessEntrypoint,
}
log.Printf("Executing: python %v", strings.Join(args, " "))
log.Fatalf("Python exited: %v", execx.Execute("python", args...))
}
-// pipInstall runs pip install with the given args.
-func pipInstall(args []string) error {
- return execx.Execute("pip", append([]string{"install"}, args...)...)
+// installSetupPackages installs Beam SDK and user dependencies.
+func installSetupPackages(mds []*pbjob.ArtifactMetadata, workDir string) error
{
+ log.Printf("Installing setup packages ...")
+
+ files := make([]string, len(mds))
+ for i, v := range mds {
+ log.Printf("Found artifact: %s", v.Name)
+ files[i] = v.Name
+ }
+
+ // Install the Dataflow Python SDK and worker packages.
+ // We install the extra requirements in case of using the beam sdk.
These are ignored by pip
+ // if the user is using an SDK that does not provide these.
+ if err := pipInstallPackage(files, workDir, sdkFile, false, false,
[]string{"gcp"}); err != nil {
+ return fmt.Errorf("failed to install SDK: %v", err)
+ }
+ // The staged files will not disappear due to restarts because workDir
is a
+ // folder that is mapped to the host (and therefore survives restarts).
+ if err := pipInstallRequirements(files, workDir, requirementsFile); err
!= nil {
+ return fmt.Errorf("failed to install requirements: %v", err)
+ }
+ if err := installExtraPackages(files, extraPackagesFile, workDir); err
!= nil {
+ return fmt.Errorf("failed to install extra packages: %v", err)
+ }
+ if err := pipInstallPackage(files, workDir, workflowFile, false, true,
nil); err != nil {
+ return fmt.Errorf("failed to install workflow: %v", err)
+ }
+
+ return nil
}
// joinPaths joins the dir to every artifact path. Each / in the path is
diff --git a/sdks/python/container/piputil.go b/sdks/python/container/piputil.go
new file mode 100644
index 0000000..b227774
--- /dev/null
+++ b/sdks/python/container/piputil.go
@@ -0,0 +1,131 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "path/filepath"
+ "strings"
+
+ "github.com/apache/beam/sdks/go/pkg/beam/util/execx"
+)
+
+const (
+ pip = "/usr/local/bin/pip"
+)
+
+// pipInstallRequirements installs the given requirement, if present.
+func pipInstallRequirements(files []string, dir, name string) error {
+ for _, file := range files {
+ if file == name {
+ // We run the install process in two rounds in order to avoid as much
+ // as possible PyPI downloads. In the first round the --find-links
+ // option will make sure that only things staged in the worker will be
+ // used without following their dependencies.
+ args := []string{"install", "-r", filepath.Join(dir, name),
"--no-index", "--no-deps", "--find-links", dir}
+ if err := execx.Execute(pip, args...); err != nil {
+ return err
+ }
+ // The second install round opens up the search for packages on PyPI and
+ // also installs dependencies. The key is that if all the packages have
+ // been installed in the first round then this command will be a no-op.
+ args = []string{"install", "-r", filepath.Join(dir, name),
"--find-links", dir}
+ return execx.Execute(pip, args...)
+ }
+ }
+ return nil
+}
+
+// pipInstallPackage installs the given package, if present.
+func pipInstallPackage(files []string, dir, name string, force, optional bool,
extras []string) error {
+ for _, file := range files {
+ if file == name {
+ var packageSpec = name
+ if extras != nil {
+ packageSpec += "[" + strings.Join(extras, ",") + "]"
+ }
+ if force {
+ // We only use force reinstallation for packages specified using the
+ // --extra_package flag. In this case, we always want to use the
+ // user-specified package, overwriting any existing package already
+ // installed. At the same time, we want to avoid reinstalling any
+ // dependencies. The "pip install" command doesn't have a clean way
to do
+ // this, so we do this in two steps.
+ //
+ // First, we use the three flags "--upgrade --force-reinstall
--no-deps"
+ // to "pip install" so as to force the package to be reinstalled, while
+ // avoiding reinstallation of dependencies. Note now that if any
needed
+ // dependencies were not installed, they will still be missing.
+ //
+ // Next, we run "pip install" on the package without any flags. Since
the
+ // installed version will match the package specified, the package
itself
+ // will not be reinstalled, but its dependencies will now be resolved
and
+ // installed if necessary. This achieves our goal outlined above.
+ args := []string{"install", "--upgrade", "--force-reinstall",
"--no-deps",
+ filepath.Join(dir, packageSpec)}
+ err := execx.Execute(pip, args...)
+ if err != nil {
+ return err
+ }
+ args = []string{"install", filepath.Join(dir, packageSpec)}
+ return execx.Execute(pip, args...)
+ }
+
+ // Case when we do not perform a forced reinstall.
+ args := []string{"install", filepath.Join(dir, packageSpec)}
+ return execx.Execute(pip, args...)
+ }
+ }
+ if optional {
+ return nil
+ }
+ return errors.New("package '" + name + "' not found")
+}
+
+// installExtraPackages installs all the packages declared in the extra
+// packages manifest file.
+func installExtraPackages(files []string, extraPackagesFile, dir string) error
{
+ // First check that extra packages manifest file is present.
+ for _, file := range files {
+ if file != extraPackagesFile {
+ continue
+ }
+
+ // Found the manifest. Install extra packages.
+ manifest, err := ioutil.ReadFile(filepath.Join(dir, extraPackagesFile))
+ if err != nil {
+ return fmt.Errorf("failed to read extra packages manifest file: %v", err)
+ }
+
+ s := bufio.NewScanner(bytes.NewReader(manifest))
+ s.Split(bufio.ScanLines)
+
+ for s.Scan() {
+ extraPackage := s.Text()
+ log.Printf("Installing extra package: %s", extraPackage)
+ if err = pipInstallPackage(files, dir, extraPackage, true, false, nil);
err != nil {
+ return fmt.Errorf("failed to install extra package %s: %v",
extraPackage, err)
+ }
+ }
+ return nil
+ }
+ return nil
+}
--
To stop receiving notification emails like this one, please contact
['"[email protected]" <[email protected]>'].