Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a refcount to dl load() and close() calls #105

Merged
merged 1 commit into from
Mar 15, 2024

Conversation

klueska
Copy link
Contributor

@klueska klueska commented Mar 14, 2024

No description provided.

@klueska klueska requested a review from elezar March 14, 2024 14:05
@klueska klueska force-pushed the init-refcount branch 2 times, most recently from e9e4001 to c61a8c8 Compare March 14, 2024 14:08
Copy link
Member

@elezar elezar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks good. I thing our negative check and handling errors in this case need to be adjusted.

Added a suggestion for how to deal with this -- although I understand if we think it's a bit convoluted.

gen/nvml/lib.go Outdated
l.Lock()
defer l.Unlock()
if l.dl != nil {

l.refcount.Inc()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Does something like:

defer l.refcount.IncOnNoError(rerror)

make sense here. (adjusting the l.refcount > 1 of course).

Also, are we assured that this defer would execute before the unlock?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it depends on if we want to up the count before we do anything, or up the count after we've done everything.

Also, are we assured that this defer would execute before the unlock?

Yes. Defers are called last-in-first-out

gen/nvml/lib.go Outdated
l.Lock()
defer l.Unlock()

l.refcount.Dec()
defer l.refcount.IncOnError(rerr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about when refcount == 0 to start with and then does not get decreased to -1 (because we're not allowing negative values). If there is then an error, our post-condition for calling close is then that refcount == 1 and not refcount == 0.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I need to handle that.

Comment on lines 27 to 25
func (r *refcount) Dec() {
if *r > 0 {
(*r)--
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about:

Suggested change
func (r *refcount) Dec() {
if *r > 0 {
(*r)--
}
}
func (r *refcount) Dec() func(error) {
if *r > 0 {
(*r)--
return r.IncOnError
}
// return a no-op if we don't decrease refcount.
return func(error) {}
}

And then we would use this:

defer l.refcount.Dec()(rerr)

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is less readable to me than just having two separate calls.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant that this handles the zero case, but sure.

@klueska klueska force-pushed the init-refcount branch 3 times, most recently from 464c1d3 to 928b2bf Compare March 14, 2024 15:03
}

func (r *refcount) LastOut(f func() error) error {
if (*r) != 1 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When do we ever decrease the refcount then?

@klueska klueska force-pushed the init-refcount branch 6 times, most recently from fe7ee79 to 46f196c Compare March 14, 2024 22:56
Copy link
Member

@elezar elezar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this @klueska

Minor comments on testing top-level failure cases and using the mock properties instead of tracking calls explicitly.


dl := &dynamicLibraryMock{
OpenFunc: func() error {
numOpens++
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is required. You could use len(dl.calls.Open) and len(dl.calls.Close) directly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

diff --git a/pkg/nvml/lib_test.go b/pkg/nvml/lib_test.go
index a800b7e..ad5a01e 100644
--- a/pkg/nvml/lib_test.go
+++ b/pkg/nvml/lib_test.go
@@ -135,15 +135,11 @@ func TestLookupFromDefault(t *testing.T) {
 }
 
 func TestLoadAndCloseNesting(t *testing.T) {
-	var numOpens, numCloses int
-
 	dl := &dynamicLibraryMock{
 		OpenFunc: func() error {
-			numOpens++
 			return nil
 		},
 		CloseFunc: func() error {
-			numCloses++
 			return nil
 		},
 	}
@@ -153,22 +149,22 @@ func TestLoadAndCloseNesting(t *testing.T) {
 	GetLibrary()
 
 	// When calling load twice, the library was only opened once
-	require.Equal(t, 0, numOpens)
+	require.Equal(t, 0, len(dl.calls.Open))
 	require.Nil(t, libnvml.load())
-	require.Equal(t, 1, numOpens)
+	require.Equal(t, 1, len(dl.calls.Open))
 	require.Nil(t, libnvml.load())
-	require.Equal(t, 1, numOpens)
+	require.Equal(t, 1, len(dl.calls.Open))
 
 	// Only after calling close twice, was the library closed
-	require.Equal(t, 0, numCloses)
+	require.Equal(t, 0, len(dl.calls.Close))
 	require.Nil(t, libnvml.close())
-	require.Equal(t, 0, numCloses)
+	require.Equal(t, 0, len(dl.calls.Close))
 	require.Nil(t, libnvml.close())
-	require.Equal(t, 1, numCloses)
+	require.Equal(t, 1, len(dl.calls.Close))
 
 	// Calling close again doesn't attempt to close the library again
 	require.Nil(t, libnvml.close())
-	require.Equal(t, 1, numCloses)
+	require.Equal(t, 1, len(dl.calls.Close))
 }
 
 func setNewDynamicLibraryDuringTest(dl dynamicLibrary) func() {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integrating...


defer setNewDynamicLibraryDuringTest(dl)()
defer resetLibrary()
GetLibrary()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the GetLibrary() call required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I did, but you're right, it appears I don't. Removing it...

defer resetLibrary()
GetLibrary()

// When calling load twice, the library was only opened once
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add the following initial check too:

	// When calling close before opening the library nothing happens.
	require.Equal(t, 0, len(dl.calls.Close))
	require.Nil(t, libnvml.close())
	require.Equal(t, 0, len(dl.calls.Close))

to ensure that we don't try to close a nil library?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integrating ...

func TestLoadAndCloseNesting(t *testing.T) {
var numOpens, numCloses int

dl := &dynamicLibraryMock{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are missing test cases for when dl.Open or dl.Close returns errors.

Copy link
Member

@elezar elezar Mar 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the following to cover the basics.

diff --git a/pkg/nvml/lib_test.go b/pkg/nvml/lib_test.go
index a800b7e..ce30111 100644
--- a/pkg/nvml/lib_test.go
+++ b/pkg/nvml/lib_test.go
@@ -171,6 +171,80 @@ func TestLoadAndCloseNesting(t *testing.T) {
 	require.Equal(t, 1, numCloses)
 }
 
+func TestLoadAndCloseWithErrors(t *testing.T) {
+	testCases := []struct {
+		description           string
+		dl                    dynamicLibrary
+		expectedLoadRefcount  refcount
+		expectedCloseRefcount refcount
+	}{
+		{
+			description: "regular flow",
+			dl: &dynamicLibraryMock{
+				OpenFunc: func() error {
+					return nil
+				},
+				CloseFunc: func() error {
+					return nil
+				},
+			},
+			expectedLoadRefcount:  1,
+			expectedCloseRefcount: 0,
+		},
+		{
+			description: "open error",
+			dl: &dynamicLibraryMock{
+				OpenFunc: func() error {
+					return errors.New("")
+				},
+				CloseFunc: func() error {
+					return nil
+				},
+			},
+			expectedLoadRefcount:  0,
+			expectedCloseRefcount: 0,
+		},
+		{
+			description: "close error",
+			dl: &dynamicLibraryMock{
+				OpenFunc: func() error {
+					return nil
+				},
+				CloseFunc: func() error {
+					return errors.New("")
+				},
+			},
+			expectedLoadRefcount:  1,
+			expectedCloseRefcount: 1,
+		},
+		{
+			description: "open and close error",
+			dl: &dynamicLibraryMock{
+				OpenFunc: func() error {
+					return errors.New("")
+				},
+				CloseFunc: func() error {
+					return errors.New("")
+				},
+			},
+			expectedLoadRefcount:  0,
+			expectedCloseRefcount: 0,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.description, func(t *testing.T) {
+			defer setNewDynamicLibraryDuringTest(tc.dl)()
+			defer resetLibrary()
+
+			_ = libnvml.load()
+			require.Equal(t, tc.expectedLoadRefcount, libnvml.refcount)
+			_ = libnvml.close()
+			require.Equal(t, tc.expectedCloseRefcount, libnvml.refcount)
+		})
+	}
+}
+
 func setNewDynamicLibraryDuringTest(dl dynamicLibrary) func() {
 	original := newDynamicLibrary
 	newDynamicLibrary = func(string, int) dynamicLibrary {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integrating...

elezar
elezar previously approved these changes Mar 15, 2024
Copy link
Member

@elezar elezar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify, I think the additions that I proposed are fine as follow-ups.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants