You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@beam.apache.org by lo...@apache.org on 2023/05/15 22:14:02 UTC

[beam] branch master updated: Enable fileio.MatchContinuously to emit duplicate file if modified (#26524)

This is an automated email from the ASF dual-hosted git repository.

lostluck 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 05eba4cfc86 Enable fileio.MatchContinuously to emit duplicate file if modified (#26524)
05eba4cfc86 is described below

commit 05eba4cfc86785f18ee4b7625bbb2f564afef95b
Author: Johanna Öjeling <51...@users.noreply.github.com>
AuthorDate: Tue May 16 00:13:54 2023 +0200

    Enable fileio.MatchContinuously to emit duplicate file if modified (#26524)
---
 sdks/go/pkg/beam/io/fileio/file.go        |  9 ++-
 sdks/go/pkg/beam/io/fileio/helper_test.go | 12 ++++
 sdks/go/pkg/beam/io/fileio/match.go       | 96 +++++++++++++++++++++++++++----
 sdks/go/pkg/beam/io/fileio/match_test.go  | 51 ++++++++++------
 4 files changed, 139 insertions(+), 29 deletions(-)

diff --git a/sdks/go/pkg/beam/io/fileio/file.go b/sdks/go/pkg/beam/io/fileio/file.go
index 4ae7b3d3d07..fc7f64ff4f7 100644
--- a/sdks/go/pkg/beam/io/fileio/file.go
+++ b/sdks/go/pkg/beam/io/fileio/file.go
@@ -21,6 +21,7 @@ import (
 	"io"
 	"path/filepath"
 	"reflect"
+	"time"
 
 	"github.com/apache/beam/sdks/v2/go/pkg/beam"
 	"github.com/apache/beam/sdks/v2/go/pkg/beam/io/filesystem"
@@ -32,10 +33,12 @@ func init() {
 	beam.RegisterType(reflect.TypeOf((*ReadableFile)(nil)).Elem())
 }
 
-// FileMetadata contains metadata about a file, namely its path and size in bytes.
+// FileMetadata contains metadata about a file, namely its path, size in bytes and last modified
+// time.
 type FileMetadata struct {
-	Path string
-	Size int64
+	Path         string
+	Size         int64
+	LastModified time.Time
 }
 
 // compressionType is the type of compression used to compress a file.
diff --git a/sdks/go/pkg/beam/io/fileio/helper_test.go b/sdks/go/pkg/beam/io/fileio/helper_test.go
index e8b61ef067f..08029759593 100644
--- a/sdks/go/pkg/beam/io/fileio/helper_test.go
+++ b/sdks/go/pkg/beam/io/fileio/helper_test.go
@@ -22,6 +22,7 @@ import (
 	"os"
 	"path/filepath"
 	"testing"
+	"time"
 )
 
 // openFile opens a file for reading.
@@ -86,3 +87,14 @@ func writeGzip(t *testing.T, path string, data []byte) {
 		t.Fatal(err)
 	}
 }
+
+func modTime(t *testing.T, path string) time.Time {
+	t.Helper()
+
+	info, err := os.Stat(path)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return info.ModTime()
+}
diff --git a/sdks/go/pkg/beam/io/fileio/match.go b/sdks/go/pkg/beam/io/fileio/match.go
index aeee887f7a1..dbd4b215025 100644
--- a/sdks/go/pkg/beam/io/fileio/match.go
+++ b/sdks/go/pkg/beam/io/fileio/match.go
@@ -27,6 +27,7 @@ import (
 	"github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/window"
 	"github.com/apache/beam/sdks/v2/go/pkg/beam/core/state"
 	"github.com/apache/beam/sdks/v2/go/pkg/beam/io/filesystem"
+	"github.com/apache/beam/sdks/v2/go/pkg/beam/log"
 	"github.com/apache/beam/sdks/v2/go/pkg/beam/register"
 	"github.com/apache/beam/sdks/v2/go/pkg/beam/transforms/periodic"
 )
@@ -37,6 +38,9 @@ func init() {
 	register.DoFn4x1[state.Provider, string, FileMetadata, func(FileMetadata), error](
 		&dedupFn{},
 	)
+	register.DoFn4x1[state.Provider, string, FileMetadata, func(FileMetadata), error](
+		&dedupUnmodifiedFn{},
+	)
 	register.Emitter1[FileMetadata]()
 	register.Emitter1[string]()
 	register.Function1x2[FileMetadata, string, FileMetadata](keyByPath)
@@ -190,21 +194,45 @@ func metadataFromFiles(
 			return nil, err
 		}
 
+		mTime, err := lastModified(ctx, fs, path)
+		if err != nil {
+			return nil, err
+		}
+
 		metadata[i] = FileMetadata{
-			Path: path,
-			Size: size,
+			Path:         path,
+			Size:         size,
+			LastModified: mTime,
 		}
 	}
 
 	return metadata, nil
 }
 
+func lastModified(ctx context.Context, fs filesystem.Interface, path string) (time.Time, error) {
+	lmGetter, ok := fs.(filesystem.LastModifiedGetter)
+	if !ok {
+		log.Warnf(ctx, "Filesystem %T does not implement filesystem.LastModifiedGetter", fs)
+		return time.Time{}, nil
+	}
+
+	mTime, err := lmGetter.LastModified(ctx, path)
+	if err != nil {
+		return time.Time{}, fmt.Errorf("error getting last modified time for %q: %v", path, err)
+	}
+
+	return mTime, nil
+}
+
 // duplicateTreatment controls how duplicate matches are treated.
 type duplicateTreatment int
 
 const (
 	// duplicateAllow allows duplicate matches.
 	duplicateAllow duplicateTreatment = iota
+	// duplicateAllowIfModified allows duplicate matches only if the file has been modified since it
+	// was last observed.
+	duplicateAllowIfModified
 	// duplicateSkip skips duplicate matches.
 	duplicateSkip
 )
@@ -241,6 +269,14 @@ func MatchDuplicateAllow() MatchContOptionFn {
 	}
 }
 
+// MatchDuplicateAllowIfModified specifies that file path matches will be deduplicated unless the
+// file has been modified since it was last observed.
+func MatchDuplicateAllowIfModified() MatchContOptionFn {
+	return func(o *matchContOption) {
+		o.DuplicateTreatment = duplicateAllowIfModified
+	}
+}
+
 // MatchDuplicateSkip specifies that file path matches will be deduplicated.
 func MatchDuplicateSkip() MatchContOptionFn {
 	return func(o *matchContOption) {
@@ -262,6 +298,8 @@ func MatchApplyWindow() MatchContOptionFn {
 //   - Start: start time for matching files. Defaults to the current timestamp
 //   - End: end time for matching files. Defaults to the maximum timestamp
 //   - DuplicateAllow: allow emitting matches that have already been observed. Defaults to false
+//   - DuplicateAllowIfModified: allow emitting matches that have already been observed if the file
+//     has been modified since the last observation. Defaults to false
 //   - DuplicateSkip: skip emitting matches that have already been observed. Defaults to true
 //   - ApplyWindow: assign each element to an individual window with a fixed size equivalent to the
 //     interval. Defaults to false, i.e. all elements will reside in the global window
@@ -290,14 +328,7 @@ func MatchContinuously(
 	globs := beam.ParDo(s, &matchContFn{Glob: glob}, imp)
 	matches := MatchAll(s, globs, MatchEmptyAllow())
 
-	var out beam.PCollection
-
-	if option.DuplicateTreatment == duplicateAllow {
-		out = matches
-	} else {
-		keyed := beam.ParDo(s, keyByPath, matches)
-		out = beam.ParDo(s, &dedupFn{}, keyed)
-	}
+	out := dedupIfRequired(s, matches, option.DuplicateTreatment)
 
 	if option.ApplyWindow {
 		return beam.WindowInto(s, window.NewFixedWindows(interval), out)
@@ -305,6 +336,24 @@ func MatchContinuously(
 	return out
 }
 
+func dedupIfRequired(
+	s beam.Scope,
+	col beam.PCollection,
+	treatment duplicateTreatment,
+) beam.PCollection {
+	if treatment == duplicateAllow {
+		return col
+	}
+
+	keyed := beam.ParDo(s, keyByPath, col)
+
+	if treatment == duplicateAllowIfModified {
+		return beam.ParDo(s, &dedupUnmodifiedFn{}, keyed)
+	}
+
+	return beam.ParDo(s, &dedupFn{}, keyed)
+}
+
 type matchContFn struct {
 	Glob string
 }
@@ -341,3 +390,30 @@ func (fn *dedupFn) ProcessElement(
 
 	return nil
 }
+
+type dedupUnmodifiedFn struct {
+	State state.Value[int64]
+}
+
+func (fn *dedupUnmodifiedFn) ProcessElement(
+	sp state.Provider,
+	_ string,
+	md FileMetadata,
+	emit func(FileMetadata),
+) error {
+	prevMTime, ok, err := fn.State.Read(sp)
+	if err != nil {
+		return fmt.Errorf("error reading state: %v", err)
+	}
+
+	mTime := md.LastModified.UnixMilli()
+
+	if !ok || mTime > prevMTime {
+		emit(md)
+		if err := fn.State.Write(sp, mTime); err != nil {
+			return fmt.Errorf("error writing state: %v", err)
+		}
+	}
+
+	return nil
+}
diff --git a/sdks/go/pkg/beam/io/fileio/match_test.go b/sdks/go/pkg/beam/io/fileio/match_test.go
index 57b2d8cfe1c..5bc849e5057 100644
--- a/sdks/go/pkg/beam/io/fileio/match_test.go
+++ b/sdks/go/pkg/beam/io/fileio/match_test.go
@@ -55,6 +55,9 @@ func TestMatchFiles(t *testing.T) {
 		write(t, filepath.Join(testDir, tf.filename), tf.data)
 	}
 
+	fp1 := filepath.Join(testDir, "file1.txt")
+	fp2 := filepath.Join(testDir, "file2.txt")
+
 	tests := []struct {
 		name string
 		glob string
@@ -66,12 +69,14 @@ func TestMatchFiles(t *testing.T) {
 			glob: filepath.Join(dir, "*", "file*.txt"),
 			want: []any{
 				FileMetadata{
-					Path: filepath.Join(testDir, "file1.txt"),
-					Size: 5,
+					Path:         fp1,
+					Size:         5,
+					LastModified: modTime(t, fp1),
 				},
 				FileMetadata{
-					Path: filepath.Join(testDir, "file2.txt"),
-					Size: 0,
+					Path:         fp2,
+					Size:         0,
+					LastModified: modTime(t, fp2),
 				},
 			},
 		},
@@ -104,6 +109,10 @@ func TestMatchAll(t *testing.T) {
 		write(t, filepath.Join(testDir, tf.filename), tf.data)
 	}
 
+	fp1 := filepath.Join(testDir, "file1.txt")
+	fp2 := filepath.Join(testDir, "file2.txt")
+	fp3 := filepath.Join(testDir, "file3.csv")
+
 	tests := []struct {
 		name    string
 		opts    []MatchOptionFn
@@ -119,16 +128,19 @@ func TestMatchAll(t *testing.T) {
 			},
 			want: []any{
 				FileMetadata{
-					Path: filepath.Join(testDir, "file1.txt"),
-					Size: 5,
+					Path:         fp1,
+					Size:         5,
+					LastModified: modTime(t, fp1),
 				},
 				FileMetadata{
-					Path: filepath.Join(testDir, "file2.txt"),
-					Size: 0,
+					Path:         fp2,
+					Size:         0,
+					LastModified: modTime(t, fp2),
 				},
 				FileMetadata{
-					Path: filepath.Join(testDir, "file3.csv"),
-					Size: 5,
+					Path:         fp3,
+					Size:         5,
+					LastModified: modTime(t, fp3),
 				},
 			},
 		},
@@ -238,6 +250,10 @@ func Test_metadataFromFiles(t *testing.T) {
 		files[i] = file
 	}
 
+	fp1 := filepath.Join(dir, "file1.txt")
+	fp2 := filepath.Join(dir, "file2.txt")
+	fp3 := filepath.Join(dir, "file3.csv")
+
 	tests := []struct {
 		name  string
 		files []string
@@ -248,16 +264,19 @@ func Test_metadataFromFiles(t *testing.T) {
 			files: files,
 			want: []FileMetadata{
 				{
-					Path: filepath.Join(dir, "file1.txt"),
-					Size: 5,
+					Path:         fp1,
+					Size:         5,
+					LastModified: modTime(t, fp1),
 				},
 				{
-					Path: filepath.Join(dir, "file2.txt"),
-					Size: 0,
+					Path:         fp2,
+					Size:         0,
+					LastModified: modTime(t, fp2),
 				},
 				{
-					Path: filepath.Join(dir, "file3.csv"),
-					Size: 5,
+					Path:         fp3,
+					Size:         5,
+					LastModified: modTime(t, fp3),
 				},
 			},
 		},