diff --git a/internal/fs/handle/file_test.go b/internal/fs/handle/file_test.go index e3accd52f6..d074a224d1 100644 --- a/internal/fs/handle/file_test.go +++ b/internal/fs/handle/file_test.go @@ -139,7 +139,7 @@ func createFileInode( Size: uint64(len(content)), Generation: 1, MetaGeneration: 1, - Updated: clock.Now(), + Updated: gcs.TimeToNS(clock.Now()), } // Create object in the fake bucket to simulate existing GCS object @@ -1277,7 +1277,7 @@ func (t *fileTest) Test_ReadWithReadManager_FullReadSuccessWithBufferedRead() { func (t *fileTest) Test_ShouldSkipSizeChecks() { const objectSize = 100 unfinalizedObject := &gcs.MinObject{Name: "unfinalized", Size: objectSize} - finalizedObject := &gcs.MinObject{Name: "finalized", Size: objectSize, Finalized: time.Now()} + finalizedObject := &gcs.MinObject{Name: "finalized", Size: objectSize, Finalized: gcs.TimeToNS(time.Now())} directIOReadMode := util.NewOpenMode(util.ReadOnly, util.O_DIRECT) readOnlyMode := util.NewOpenMode(util.ReadOnly, 0) diff --git a/internal/fs/inode/file.go b/internal/fs/inode/file.go index 819cc26144..ac82183ce8 100644 --- a/internal/fs/inode/file.go +++ b/internal/fs/inode/file.go @@ -550,7 +550,7 @@ func (f *FileInode) Attributes( ctx context.Context, clobberedCheck bool) (attrs fuseops.InodeAttributes, err error) { attrs = f.attrs // Obtain default information from the source object. - attrs.Mtime = f.src.Updated + attrs.Mtime = f.src.UpdatedTime() attrs.Size = f.src.Size // If the source object has an mtime metadata key, use that instead of its diff --git a/internal/fs/inode/file_mock_bucket_test.go b/internal/fs/inode/file_mock_bucket_test.go index 68e3ac0e7c..ba4fe80bab 100644 --- a/internal/fs/inode/file_mock_bucket_test.go +++ b/internal/fs/inode/file_mock_bucket_test.go @@ -323,7 +323,7 @@ func (t *FileMockBucketTest) TestAttributes_NoChangeInAttributes() { Size: initialSize, Generation: initialGeneration, MetaGeneration: initialMetaGeneration, - Updated: initialTime, + Updated: gcs.TimeToNS(initialTime), } f := t.createGCSBackedFileInode(backingObj) defer f.Unlock() @@ -334,7 +334,7 @@ func (t *FileMockBucketTest) TestAttributes_NoChangeInAttributes() { Generation: initialGeneration, MetaGeneration: initialMetaGeneration, Size: initialSize, // Size is not greater - Updated: t.clock.Now(), + Updated: gcs.TimeToNS(t.clock.Now()), } statReq := &gcs.StatObjectRequest{Name: fileName, ForceFetchFromGcs: false, ReturnExtendedObjectAttributes: false} t.bucket.On("StatObject", t.ctx, statReq).Return(updatedMinObject, &gcs.ExtendedObjectAttributes{}, nil).Once() @@ -347,7 +347,7 @@ func (t *FileMockBucketTest) TestAttributes_NoChangeInAttributes() { assert.Equal(t.T(), initialSize, attrs.Size) assert.Equal(t.T(), initialTime, attrs.Mtime) assert.Equal(t.T(), initialSize, f.Source().Size) - assert.Equal(t.T(), initialTime, f.Source().Updated) + assert.Equal(t.T(), initialTime, f.Source().UpdatedTime()) } func (t *FileMockBucketTest) TestInitBufferedWriteHandlerIfEligible_ZonalBucket_DoesNotFetchMetadataFromGCS_ForAppends() { diff --git a/internal/fs/inode/file_test.go b/internal/fs/inode/file_test.go index 0aba6cebf7..912b79870e 100644 --- a/internal/fs/inode/file_test.go +++ b/internal/fs/inode/file_test.go @@ -403,9 +403,9 @@ func (t *FileTest) TestInitialAttributes() { assert.Equal(t.T(), uint32(uid), attrs.Uid) assert.Equal(t.T(), uint32(gid), attrs.Gid) assert.Equal(t.T(), fileMode, attrs.Mode) - assert.Equal(t.T(), attrs.Atime, t.backingObj.Updated) - assert.Equal(t.T(), attrs.Ctime, t.backingObj.Updated) - assert.Equal(t.T(), attrs.Mtime, t.backingObj.Updated) + assert.Equal(t.T(), attrs.Atime, t.backingObj.UpdatedTime()) + assert.Equal(t.T(), attrs.Ctime, t.backingObj.UpdatedTime()) + assert.Equal(t.T(), attrs.Mtime, t.backingObj.UpdatedTime()) } func (t *FileTest) TestInitialAttributes_MtimeFromObjectMetadata_Gcsfuse() { diff --git a/internal/fs/inode/symlink.go b/internal/fs/inode/symlink.go index c01288342d..45e50efa81 100644 --- a/internal/fs/inode/symlink.go +++ b/internal/fs/inode/symlink.go @@ -102,9 +102,9 @@ func NewSymlinkInode( Uid: attrs.Uid, Gid: attrs.Gid, Mode: attrs.Mode, - Atime: m.Updated, - Ctime: m.Updated, - Mtime: m.Updated, + Atime: m.UpdatedTime(), + Ctime: m.UpdatedTime(), + Mtime: m.UpdatedTime(), }, metadata: m.Metadata, } @@ -265,6 +265,6 @@ func (s *SymlinkInode) Source() *gcs.MinObject { MetaGeneration: s.sourceGeneration.Metadata, Size: s.sourceGeneration.Size, Metadata: s.metadata, - Updated: s.attrs.Mtime, + Updated: gcs.TimeToNS(s.attrs.Mtime), } } diff --git a/internal/fs/inode/symlink_test.go b/internal/fs/inode/symlink_test.go index 9d75f24009..5a8cc78fe0 100644 --- a/internal/fs/inode/symlink_test.go +++ b/internal/fs/inode/symlink_test.go @@ -172,7 +172,7 @@ func (t *SymlinkTest) TestSource() { AssertEq(nil, err) m := storageutil.ConvertObjToMinObject(obj) m.Metadata = map[string]string{inode.StandardSymlinkMetadataKey: "true"} - m.Updated = time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) // Explicitly set Updated time for consistent testing. + m.Updated = time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixNano() // Explicitly set Updated time for consistent testing. attrs := fuseops.InodeAttributes{} name := inode.NewFileName(inode.NewRootName("some-bucket"), m.Name) s, err := inode.NewSymlinkInode(context.Background(), fuseops.InodeID(42), name, t.bucket, m, attrs) @@ -185,5 +185,5 @@ func (t *SymlinkTest) TestSource() { AssertEq(m.MetaGeneration, source.MetaGeneration) AssertEq(m.Size, source.Size) AssertEq(m.Metadata, source.Metadata) - AssertEq(0, m.Updated.Compare(source.Updated)) + AssertEq(m.Updated, source.Updated) } diff --git a/internal/fs/local_modifications_test.go b/internal/fs/local_modifications_test.go index 8e96b5983e..02d8433667 100644 --- a/internal/fs/local_modifications_test.go +++ b/internal/fs/local_modifications_test.go @@ -1547,7 +1547,7 @@ func validateObjectAttributes(extendedAttr1, extendedAttr2 *gcs.ExtendedObjectAt ExpectEq(0, minObject1.Size) ExpectEq(FileContentsSize, minObject2.Size) ExpectNe(minObject1.Generation, minObject2.Generation) - ExpectTrue(minObject1.Updated.Before(minObject2.Updated)) + ExpectTrue(minObject1.Updated < minObject2.Updated) attr1MTime, _ := time.Parse(time.RFC3339Nano, minObject1.Metadata[gcs.MtimeMetadataKey]) attr2MTime, _ := time.Parse(time.RFC3339Nano, minObject2.Metadata[gcs.MtimeMetadataKey]) ExpectTrue(attr1MTime.Before(attr2MTime)) diff --git a/internal/gcsx/file_cache_reader_test.go b/internal/gcsx/file_cache_reader_test.go index ef7c66a12e..23c921db6f 100644 --- a/internal/gcsx/file_cache_reader_test.go +++ b/internal/gcsx/file_cache_reader_test.go @@ -90,7 +90,7 @@ func (t *fileCacheReaderTest) SetupTest() { Name: testObject, Size: 17, Generation: 1234, - Finalized: time.Date(2025, time.June, 27, 07, 22, 30, 0, time.UTC), + Finalized: gcs.TimeToNS(time.Date(2025, time.June, 27, 07, 22, 30, 0, time.UTC)), } t.unfinalized_object = &gcs.MinObject{ Name: testObject_unfinalized, diff --git a/internal/gcsx/garbage_collect.go b/internal/gcsx/garbage_collect.go index db842fa746..e7e805c72d 100644 --- a/internal/gcsx/garbage_collect.go +++ b/internal/gcsx/garbage_collect.go @@ -53,7 +53,7 @@ func garbageCollectOnce( group.Go(func() (err error) { defer close(staleNames) for o := range minObjects { - if now.Sub(o.Updated) < stalenessThreshold { + if now.Sub(o.UpdatedTime()) < stalenessThreshold { continue } diff --git a/internal/storage/fake/bucket.go b/internal/storage/fake/bucket.go index f22e821dd7..c3daecc381 100644 --- a/internal/storage/fake/bucket.go +++ b/internal/storage/fake/bucket.go @@ -536,8 +536,8 @@ func copyMinObject(o *gcs.Object) *gcs.MinObject { copy.Size = o.Size copy.Generation = o.Generation copy.MetaGeneration = o.MetaGeneration - copy.Updated = o.Updated - copy.Finalized = o.Finalized + copy.Updated = gcs.TimeToNS(o.Updated) + copy.Finalized = gcs.TimeToNS(o.Finalized) copy.Metadata = copyMetadata(o.Metadata) copy.ContentEncoding = o.ContentEncoding copy.CRC32C = o.CRC32C diff --git a/internal/storage/fake/testing/bucket_tests.go b/internal/storage/fake/testing/bucket_tests.go index 9dd9847075..553419e991 100644 --- a/internal/storage/fake/testing/bucket_tests.go +++ b/internal/storage/fake/testing/bucket_tests.go @@ -506,8 +506,8 @@ func (t *bucketTest) assertOnObjectAttributes(expectedMinObj *gcs.MinObject, exp ExpectThat(expectedMinObj.Size, Equals(o.Size)) ExpectThat(expectedMinObj.Generation, Equals(o.Generation)) ExpectThat(expectedMinObj.MetaGeneration, Equals(o.MetaGeneration)) - ExpectThat(expectedMinObj.Updated, DeepEquals(o.Updated)) - ExpectThat(expectedMinObj.Finalized, DeepEquals(o.Finalized)) + ExpectThat(expectedMinObj.UpdatedTime(), DeepEquals(o.Updated)) + ExpectThat(expectedMinObj.FinalizedTime(), DeepEquals(o.Finalized)) ExpectThat(expectedMinObj.Metadata, DeepEquals(o.Metadata)) ExpectThat(expectedMinObj.ContentEncoding, Equals(o.ContentEncoding)) ExpectThat(expectedMinObj.CRC32C, Equals(o.CRC32C)) @@ -3490,8 +3490,8 @@ func (t *statTest) StatAfterCreating() { ExpectEq(orig.Generation, m.Generation) ExpectEq(len("taco"), m.Size) ExpectThat(e.Deleted, timeutil.TimeEq(time.Time{})) - ExpectThat(m.Updated, timeutil.TimeEq(orig.Updated)) - ExpectThat(m.Finalized, timeutil.TimeEq(time.Time{})) + ExpectThat(m.UpdatedTime(), timeutil.TimeEq(orig.Updated)) + ExpectThat(m.FinalizedTime(), timeutil.TimeEq(time.Time{})) } func (t *statTest) StatAfterOverwriting() { @@ -3527,8 +3527,8 @@ func (t *statTest) StatAfterOverwriting() { ExpectEq(o2.Generation, m.Generation) ExpectEq(len("burrito"), m.Size) ExpectThat(e.Deleted, timeutil.TimeEq(time.Time{})) - ExpectThat(m.Updated, timeutil.TimeEq(o2.Updated)) - ExpectThat(m.Finalized, timeutil.TimeEq(time.Time{})) + ExpectThat(m.UpdatedTime(), timeutil.TimeEq(o2.Updated)) + ExpectThat(m.FinalizedTime(), timeutil.TimeEq(time.Time{})) } func (t *statTest) StatAfterUpdating() { @@ -3581,8 +3581,8 @@ func (t *statTest) StatAfterUpdating() { ExpectEq(o2.MetaGeneration, m.MetaGeneration) ExpectEq(len("taco"), m.Size) ExpectThat(e.Deleted, timeutil.TimeEq(time.Time{})) - ExpectThat(m.Updated, timeutil.TimeEq(o2.Updated)) - ExpectThat(m.Finalized, timeutil.TimeEq(time.Time{})) + ExpectThat(m.UpdatedTime(), timeutil.TimeEq(o2.Updated)) + ExpectThat(m.FinalizedTime(), timeutil.TimeEq(time.Time{})) } //////////////////////////////////////////////////////////////////////// diff --git a/internal/storage/gcs/object.go b/internal/storage/gcs/object.go index 33b5dd100d..b32a649003 100644 --- a/internal/storage/gcs/object.go +++ b/internal/storage/gcs/object.go @@ -86,8 +86,8 @@ type MinObject struct { Size uint64 Generation int64 MetaGeneration int64 - Updated time.Time - Finalized time.Time + Updated int64 // Unix nanoseconds, 0 if zero + Finalized int64 // Unix nanoseconds, 0 if zero Metadata map[string]string ContentEncoding string CRC32C *uint32 // Missing for CMEK buckets @@ -119,5 +119,32 @@ func (mo MinObject) HasContentEncodingGzip() bool { } func (mo MinObject) IsUnfinalized() bool { - return mo.Finalized.IsZero() + return mo.Finalized == 0 +} + +// UpdatedTime returns the time.Time representation of the Updated timestamp. +func (mo MinObject) UpdatedTime() time.Time { + return NSToTime(mo.Updated) +} + +// FinalizedTime returns the time.Time representation of the Finalized timestamp. +func (mo MinObject) FinalizedTime() time.Time { + return NSToTime(mo.Finalized) +} + +// TimeToNS converts a time.Time to Unix nanoseconds, returning 0 if the time is zero. +// This avoids panics when calling UnixNano() on a zero time.Time. +func TimeToNS(t time.Time) int64 { + if t.IsZero() { + return 0 + } + return t.UnixNano() +} + +// NSToTime converts Unix nanoseconds to time.Time, returning a zero time.Time if ns is 0. +func NSToTime(ns int64) time.Time { + if ns == 0 { + return time.Time{} + } + return time.Unix(0, ns) } diff --git a/internal/storage/gcs/object_test.go b/internal/storage/gcs/object_test.go index 2d1b335680..43a4ef58d5 100644 --- a/internal/storage/gcs/object_test.go +++ b/internal/storage/gcs/object_test.go @@ -40,7 +40,7 @@ func TestHasContentEncodingGzipNegative(t *testing.T) { } func TestIsFinalized(t *testing.T) { - mo := MinObject{Finalized: time.Date(2025, time.June, 19, 18, 23, 30, 0, time.UTC)} + mo := MinObject{Finalized: TimeToNS(time.Date(2025, time.June, 19, 18, 23, 30, 0, time.UTC))} assert.False(t, mo.IsUnfinalized()) } diff --git a/internal/storage/storageutil/object_attrs.go b/internal/storage/storageutil/object_attrs.go index 66f7189655..2a7293b796 100644 --- a/internal/storage/storageutil/object_attrs.go +++ b/internal/storage/storageutil/object_attrs.go @@ -122,8 +122,8 @@ func ObjectAttrsToMinObject(attrs *storage.ObjectAttrs) *gcs.MinObject { Metadata: attrs.Metadata, Generation: attrs.Generation, MetaGeneration: attrs.Metageneration, - Updated: attrs.Updated, - Finalized: attrs.Finalized, + Updated: gcs.TimeToNS(attrs.Updated), + Finalized: gcs.TimeToNS(attrs.Finalized), } } @@ -170,11 +170,11 @@ func ConvertObjToMinObject(o *gcs.Object) *gcs.MinObject { Size: o.Size, Generation: o.Generation, MetaGeneration: o.MetaGeneration, - Updated: o.Updated, + Updated: gcs.TimeToNS(o.Updated), Metadata: o.Metadata, ContentEncoding: o.ContentEncoding, CRC32C: o.CRC32C, - Finalized: o.Finalized, + Finalized: gcs.TimeToNS(o.Finalized), } } @@ -211,8 +211,8 @@ func ConvertMinObjectAndExtendedObjectAttributesToObject(m *gcs.MinObject, Size: m.Size, Generation: m.Generation, MetaGeneration: m.MetaGeneration, - Updated: m.Updated, - Finalized: m.Finalized, + Updated: m.UpdatedTime(), + Finalized: m.FinalizedTime(), Metadata: m.Metadata, ContentEncoding: m.ContentEncoding, ContentType: e.ContentType, @@ -242,10 +242,10 @@ func ConvertMinObjectToObject(m *gcs.MinObject) *gcs.Object { Size: m.Size, Generation: m.Generation, MetaGeneration: m.MetaGeneration, - Updated: m.Updated, + Updated: m.UpdatedTime(), Metadata: m.Metadata, ContentEncoding: m.ContentEncoding, CRC32C: m.CRC32C, - Finalized: m.Finalized, + Finalized: m.FinalizedTime(), } } diff --git a/internal/storage/storageutil/object_attrs_test.go b/internal/storage/storageutil/object_attrs_test.go index 8ff61a2cdf..cbeaac2cbc 100644 --- a/internal/storage/storageutil/object_attrs_test.go +++ b/internal/storage/storageutil/object_attrs_test.go @@ -250,8 +250,8 @@ func Test_ConvertObjToMinObject_WithValidObject(t *testing.T) { assert.Equal(t, size, gcsMinObject.Size) assert.Equal(t, generation, gcsMinObject.Generation) assert.Equal(t, metaGeneration, gcsMinObject.MetaGeneration) - assert.True(t, currentTime.Equal(gcsMinObject.Updated)) - assert.True(t, currentTime.Equal(gcsMinObject.Finalized)) + assert.True(t, currentTime.Equal(gcsMinObject.UpdatedTime())) + assert.True(t, currentTime.Equal(gcsMinObject.FinalizedTime())) assert.Equal(t, contentEncode, gcsMinObject.ContentEncoding) assert.Equal(t, metadata, gcsMinObject.Metadata) assert.Equal(t, crc32C, *gcsMinObject.CRC32C) @@ -342,8 +342,8 @@ func Test_ConvertObjToExtendedObjectAttributes_WithNonNilMinObjectAndNonNilAttri Size: uint64(36), Generation: int64(444), MetaGeneration: int64(555), - Updated: timeAttr, - Finalized: timeAttr, + Updated: gcs.TimeToNS(timeAttr), + Finalized: gcs.TimeToNS(timeAttr), Metadata: map[string]string{"test_key": "test_value"}, ContentEncoding: "test_encoding", } @@ -370,8 +370,8 @@ func Test_ConvertObjToExtendedObjectAttributes_WithNonNilMinObjectAndNonNilAttri assert.Equal(t, minObject.Size, gcsObject.Size) assert.Equal(t, minObject.Generation, gcsObject.Generation) assert.Equal(t, minObject.MetaGeneration, gcsObject.MetaGeneration) - assert.Equal(t, 0, gcsObject.Updated.Compare(minObject.Updated)) - assert.Equal(t, 0, gcsObject.Finalized.Compare(minObject.Finalized)) + assert.Equal(t, 0, gcsObject.Updated.Compare(minObject.UpdatedTime())) + assert.Equal(t, 0, gcsObject.Finalized.Compare(minObject.FinalizedTime())) assert.Equal(t, minObject.Metadata, gcsObject.Metadata) assert.Equal(t, minObject.ContentEncoding, gcsObject.ContentEncoding) assert.Equal(t, extendedObjAttr.ContentType, gcsObject.ContentType) @@ -406,8 +406,8 @@ func Test_ConvertMinObjectToObject_WithNonNilMinObject(t *testing.T) { Size: uint64(36), Generation: int64(444), MetaGeneration: int64(555), - Updated: timeAttr, - Finalized: timeAttr, + Updated: gcs.TimeToNS(timeAttr), + Finalized: gcs.TimeToNS(timeAttr), Metadata: map[string]string{"test_key": "test_value"}, ContentEncoding: "test_encoding", CRC32C: &crc32C, @@ -420,8 +420,8 @@ func Test_ConvertMinObjectToObject_WithNonNilMinObject(t *testing.T) { assert.Equal(t, minObject.Size, gcsObject.Size) assert.Equal(t, minObject.Generation, gcsObject.Generation) assert.Equal(t, minObject.MetaGeneration, gcsObject.MetaGeneration) - assert.Equal(t, 0, gcsObject.Updated.Compare(minObject.Updated)) - assert.Equal(t, 0, gcsObject.Finalized.Compare(minObject.Finalized)) + assert.Equal(t, 0, gcsObject.Updated.Compare(minObject.UpdatedTime())) + assert.Equal(t, 0, gcsObject.Finalized.Compare(minObject.FinalizedTime())) assert.Equal(t, minObject.Metadata, gcsObject.Metadata) assert.Equal(t, minObject.ContentEncoding, gcsObject.ContentEncoding) assert.Equal(t, "", gcsObject.ContentType) diff --git a/internal/util/sizeof_test.go b/internal/util/sizeof_test.go index 58ee84b67b..41665d6b69 100644 --- a/internal/util/sizeof_test.go +++ b/internal/util/sizeof_test.go @@ -271,7 +271,7 @@ func TestNestedSizeOfGcsMinObject(t *testing.T) { Metadata: customMetadataFields, Generation: generation, MetaGeneration: metaGeneration, - Updated: updated, + Updated: gcs.TimeToNS(updated), CRC32C: &crc32, }