data wiped from MemoryStore after store.close + array[:] - #2067

Closed
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close
Closed

data wiped from MemoryStore after store.close + array[:]#2067
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close

Conversation

@d-v-b

@d-v-bd-v-b commented Aug 6, 2024

Copy link
Copy Markdown
Contributor

Over in #1746@dcherian discovered the following bug:

importzarrfromzarr.arrayimportArrayfromzarr.groupimportGroupfromzarr.storeimportMemoryStoreimportnumpyasnpstore=MemoryStore(mode="w")
root=Group.create(store)
nparray=np.array([1], dtype=np.int8)
a=root.create_array(
"/0/0",
shape=nparray.shape,
chunks=(1,),
dtype=nparray.dtype.str,
attributes={},
# compressor=compressor, # TODO: FIXMEfill_value=nparray.dtype.type(0),
)
a[:] =nparrayprint(a[:]) # [1]store.close()
print(a[:]) # [0]

Setting data with a[:] = nparray, followed by a.store_path.store.close(), followed by a[:], results in the store getting wiped. This is not intended behavior.

this PR adds:

  • type hints to the indexing tests
  • a test for the aforementioned bug, which is currently failing, because we have not fixed it yet. this PR should be updated when we fix the bug.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

Closes#2067

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

so i figured out where this is coming from. i'm not sure if it's a bug or not.

  1. we close the store, which sets is_open to False.
  2. a[:] invokes MemoryStore.get(), which hits this line:
ifnotself._is_open:
awaitself._open()
  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

I don't fully understand the open / closed semantics here. My intuition is that re-opening a closed store feels weird, and having the store._open() routine invoked outside of Store.__init__ feels incorrect.

@brokkoli71 what's the logic for invoking Store._open in the get and set methods? I feel like it might be cleaner to only open the store in 1 place (__init__), and attempting to call Store.get/set on a closed store should simply error.

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

tests are now passing with this PR. Instead of conditionally calling store._open() in store.get or store.set, a RuntimeError is raised when attempting to use these methods on a store that is closed. I also adjusted all the tests to use sync(Store.open()) instead of Store().

I think we should not merge this until we have a conversation about the store design. I would really like to be able to create store classes in synchronous code without needing sync, but we can't do that now because (some) store initialization requires IO, which is async.

@jhamman and @normanrz any thoughts about this issue (i.e., that we cannot fully initialize a store class in synchronous code without sync)?

@dcherian

Copy link
Copy Markdown
Contributor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

The bug was that the store class self-destructs if a) it was created with overwrite=True, and b) you attempt to do any array indexing after closing the store.

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for working on this @d-v-b!

Comment threadsrc/zarr/store/memory.py Outdated
Comment on lines +73 to +74
if not self._is_open:
await self._open()
raise RuntimeError("Store is closed. Cannot `get` from a closed store.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

turn this into self._check_open() like self._check_writable()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also, should we propagate this to all other stores?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

both good ideas! the fact that tests passed without me making the last change means we need more tests for this

@jhammanjhamman added the V3 label Aug 9, 2024
@brokkoli71

brokkoli71 commented Aug 12, 2024

Copy link
Copy Markdown
Contributor

@d-v-b@dcherian

  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

In this example the mode was explicitly set to "w" which will overwrite the data when opening a store. The default value of MemoryStore() is mode="r" which will raise a ValueError in this code snippet as it does not allow writing. I think, what the code snippet was trying to achieve was for reusing a closed array which can be done with mode "r+" or "a".
I'd say this illustrates that the effect of the AccessModeLiterals should be communicated better. Currently only the asynchronous open method contains docstrings explaining:

 mode : {'r', 'r+', 'a', 'w', 'w-'}, optional
Persistence mode: 'r' means read only (must exist); 'r+' means
read/write (must exist); 'a' means read/write (create if doesn't
exist); 'w' means create (overwrite if exists); 'w-' means create
(fail if exists).

Furthermore, the examples in the docstrings of array.py suggest using mode="w". I think overwriting stores on opening them has it's usecase but should not be communicated as a general good idea.

The other problem with implicitly opening a store is a problem, too. I agree with the comments above.

Edit:
Also, in store/core.py::make_store_path the mode will fallback to "w":

 elif store_like is None:
if mode is None:
mode = "w" # exception to the default mode = 'r'

I think it make sense here to fallback to "a", too

Comment threadsrc/zarr/store/memory.py Outdated
Co-authored-by: Hannes Spitz <44113112+brokkoli71@users.noreply.github.com>
@brokkoli71

Copy link
Copy Markdown
Contributor

I had a similar thought process while working on #2000. On the one hand sync(Store.open()) seems a bit much for the API, on the other hand implicitly calling _open might cause confusion. Currently the only modification of _open on the array is clearing it if overwrite is enabled. Maybe if we make that behavior more clear to the user that might not be a problem? what do you think? @d-v-b

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

@TomAugspurger do you have any ideas for how we could address this cleanly? for background, I am not happy with the solution I am pursuing in this PR, so we shouldn't give that much weight

@TomAugspurger

Copy link
Copy Markdown
Contributor

I don't have a strong opinion after thinking through this for a bit, but here's something:

I wonder if we have two different kinds of "opening" (and closing) going on here:

  1. A "resource"-level open / close: manage database connections, acquire locks, etc, which must be done before any operation can succeed.
  2. A logical open / close: do the stuff you need to do when you open (or close) a Store to match the semantics we want. Which I think is just clear non-empty stores with mode="w" and error on non-empty stores with mode="w-".

IMO, the logical stuff should just happen the very first time a store is created using Store.open. And I think that's the only time our current Store._open (which does the clearing for mode="w") should be called.

The resource-level stuff can happen whenever I think (perhaps with APIs for users to control).

This is all assuming that the Stores are created with MyStore.open, which maybe isn't reasonable (but we could force it). I dunno...

@jhamman
jhamman changed the base branch from v3 to mainOctober 14, 2024 20:58
@jhammanjhamman added this to the After 3.0.0 milestone Oct 17, 2024
@jhamman

Copy link
Copy Markdown
Member

@d-v-b - 75% sure this can be closed now that #2442 is in. What do you think?

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

i agree!

@d-v-bd-v-b closed this Nov 13, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants

@d-v-b@dcherian@brokkoli71@TomAugspurger@jhamman
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

data wiped from MemoryStore after store.close + array[:] - #2067

Closed
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close
Closed

data wiped from MemoryStore after store.close + array[:]#2067
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close

Conversation

@d-v-b

@d-v-bd-v-b commented Aug 6, 2024

Copy link
Copy Markdown
Contributor

Over in #1746@dcherian discovered the following bug:

importzarrfromzarr.arrayimportArrayfromzarr.groupimportGroupfromzarr.storeimportMemoryStoreimportnumpyasnpstore=MemoryStore(mode="w")
root=Group.create(store)
nparray=np.array([1], dtype=np.int8)
a=root.create_array(
"/0/0",
shape=nparray.shape,
chunks=(1,),
dtype=nparray.dtype.str,
attributes={},
# compressor=compressor, # TODO: FIXMEfill_value=nparray.dtype.type(0),
)
a[:] =nparrayprint(a[:]) # [1]store.close()
print(a[:]) # [0]

Setting data with a[:] = nparray, followed by a.store_path.store.close(), followed by a[:], results in the store getting wiped. This is not intended behavior.

this PR adds:

  • type hints to the indexing tests
  • a test for the aforementioned bug, which is currently failing, because we have not fixed it yet. this PR should be updated when we fix the bug.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

Closes#2067

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

so i figured out where this is coming from. i'm not sure if it's a bug or not.

  1. we close the store, which sets is_open to False.
  2. a[:] invokes MemoryStore.get(), which hits this line:
ifnotself._is_open:
awaitself._open()
  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

I don't fully understand the open / closed semantics here. My intuition is that re-opening a closed store feels weird, and having the store._open() routine invoked outside of Store.__init__ feels incorrect.

@brokkoli71 what's the logic for invoking Store._open in the get and set methods? I feel like it might be cleaner to only open the store in 1 place (__init__), and attempting to call Store.get/set on a closed store should simply error.

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

tests are now passing with this PR. Instead of conditionally calling store._open() in store.get or store.set, a RuntimeError is raised when attempting to use these methods on a store that is closed. I also adjusted all the tests to use sync(Store.open()) instead of Store().

I think we should not merge this until we have a conversation about the store design. I would really like to be able to create store classes in synchronous code without needing sync, but we can't do that now because (some) store initialization requires IO, which is async.

@jhamman and @normanrz any thoughts about this issue (i.e., that we cannot fully initialize a store class in synchronous code without sync)?

@dcherian

Copy link
Copy Markdown
Contributor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

The bug was that the store class self-destructs if a) it was created with overwrite=True, and b) you attempt to do any array indexing after closing the store.

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for working on this @d-v-b!

Comment threadsrc/zarr/store/memory.py Outdated
Comment on lines +73 to +74
if not self._is_open:
await self._open()
raise RuntimeError("Store is closed. Cannot `get` from a closed store.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

turn this into self._check_open() like self._check_writable()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also, should we propagate this to all other stores?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

both good ideas! the fact that tests passed without me making the last change means we need more tests for this

@jhammanjhamman added the V3 label Aug 9, 2024
@brokkoli71

brokkoli71 commented Aug 12, 2024

Copy link
Copy Markdown
Contributor

@d-v-b@dcherian

  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

In this example the mode was explicitly set to "w" which will overwrite the data when opening a store. The default value of MemoryStore() is mode="r" which will raise a ValueError in this code snippet as it does not allow writing. I think, what the code snippet was trying to achieve was for reusing a closed array which can be done with mode "r+" or "a".
I'd say this illustrates that the effect of the AccessModeLiterals should be communicated better. Currently only the asynchronous open method contains docstrings explaining:

 mode : {'r', 'r+', 'a', 'w', 'w-'}, optional
Persistence mode: 'r' means read only (must exist); 'r+' means
read/write (must exist); 'a' means read/write (create if doesn't
exist); 'w' means create (overwrite if exists); 'w-' means create
(fail if exists).

Furthermore, the examples in the docstrings of array.py suggest using mode="w". I think overwriting stores on opening them has it's usecase but should not be communicated as a general good idea.

The other problem with implicitly opening a store is a problem, too. I agree with the comments above.

Edit:
Also, in store/core.py::make_store_path the mode will fallback to "w":

 elif store_like is None:
if mode is None:
mode = "w" # exception to the default mode = 'r'

I think it make sense here to fallback to "a", too

Comment threadsrc/zarr/store/memory.py Outdated
Co-authored-by: Hannes Spitz <44113112+brokkoli71@users.noreply.github.com>
@brokkoli71

Copy link
Copy Markdown
Contributor

I had a similar thought process while working on #2000. On the one hand sync(Store.open()) seems a bit much for the API, on the other hand implicitly calling _open might cause confusion. Currently the only modification of _open on the array is clearing it if overwrite is enabled. Maybe if we make that behavior more clear to the user that might not be a problem? what do you think? @d-v-b

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

@TomAugspurger do you have any ideas for how we could address this cleanly? for background, I am not happy with the solution I am pursuing in this PR, so we shouldn't give that much weight

@TomAugspurger

Copy link
Copy Markdown
Contributor

I don't have a strong opinion after thinking through this for a bit, but here's something:

I wonder if we have two different kinds of "opening" (and closing) going on here:

  1. A "resource"-level open / close: manage database connections, acquire locks, etc, which must be done before any operation can succeed.
  2. A logical open / close: do the stuff you need to do when you open (or close) a Store to match the semantics we want. Which I think is just clear non-empty stores with mode="w" and error on non-empty stores with mode="w-".

IMO, the logical stuff should just happen the very first time a store is created using Store.open. And I think that's the only time our current Store._open (which does the clearing for mode="w") should be called.

The resource-level stuff can happen whenever I think (perhaps with APIs for users to control).

This is all assuming that the Stores are created with MyStore.open, which maybe isn't reasonable (but we could force it). I dunno...

@jhamman
jhamman changed the base branch from v3 to mainOctober 14, 2024 20:58
@jhammanjhamman added this to the After 3.0.0 milestone Oct 17, 2024
@jhamman

Copy link
Copy Markdown
Member

@d-v-b - 75% sure this can be closed now that #2442 is in. What do you think?

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

i agree!

@d-v-bd-v-b closed this Nov 13, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants

@d-v-b@dcherian@brokkoli71@TomAugspurger@jhamman
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

data wiped from MemoryStore after store.close + array[:] - #2067

Closed
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close
Closed

data wiped from MemoryStore after store.close + array[:]#2067
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close

Conversation

@d-v-b

@d-v-bd-v-b commented Aug 6, 2024

Copy link
Copy Markdown
Contributor

Over in #1746@dcherian discovered the following bug:

importzarrfromzarr.arrayimportArrayfromzarr.groupimportGroupfromzarr.storeimportMemoryStoreimportnumpyasnpstore=MemoryStore(mode="w")
root=Group.create(store)
nparray=np.array([1], dtype=np.int8)
a=root.create_array(
"/0/0",
shape=nparray.shape,
chunks=(1,),
dtype=nparray.dtype.str,
attributes={},
# compressor=compressor, # TODO: FIXMEfill_value=nparray.dtype.type(0),
)
a[:] =nparrayprint(a[:]) # [1]store.close()
print(a[:]) # [0]

Setting data with a[:] = nparray, followed by a.store_path.store.close(), followed by a[:], results in the store getting wiped. This is not intended behavior.

this PR adds:

  • type hints to the indexing tests
  • a test for the aforementioned bug, which is currently failing, because we have not fixed it yet. this PR should be updated when we fix the bug.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

Closes#2067

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

so i figured out where this is coming from. i'm not sure if it's a bug or not.

  1. we close the store, which sets is_open to False.
  2. a[:] invokes MemoryStore.get(), which hits this line:
ifnotself._is_open:
awaitself._open()
  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

I don't fully understand the open / closed semantics here. My intuition is that re-opening a closed store feels weird, and having the store._open() routine invoked outside of Store.__init__ feels incorrect.

@brokkoli71 what's the logic for invoking Store._open in the get and set methods? I feel like it might be cleaner to only open the store in 1 place (__init__), and attempting to call Store.get/set on a closed store should simply error.

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

tests are now passing with this PR. Instead of conditionally calling store._open() in store.get or store.set, a RuntimeError is raised when attempting to use these methods on a store that is closed. I also adjusted all the tests to use sync(Store.open()) instead of Store().

I think we should not merge this until we have a conversation about the store design. I would really like to be able to create store classes in synchronous code without needing sync, but we can't do that now because (some) store initialization requires IO, which is async.

@jhamman and @normanrz any thoughts about this issue (i.e., that we cannot fully initialize a store class in synchronous code without sync)?

@dcherian

Copy link
Copy Markdown
Contributor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

The bug was that the store class self-destructs if a) it was created with overwrite=True, and b) you attempt to do any array indexing after closing the store.

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for working on this @d-v-b!

Comment threadsrc/zarr/store/memory.py Outdated
Comment on lines +73 to +74
if not self._is_open:
await self._open()
raise RuntimeError("Store is closed. Cannot `get` from a closed store.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

turn this into self._check_open() like self._check_writable()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also, should we propagate this to all other stores?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

both good ideas! the fact that tests passed without me making the last change means we need more tests for this

@jhammanjhamman added the V3 label Aug 9, 2024
@brokkoli71

brokkoli71 commented Aug 12, 2024

Copy link
Copy Markdown
Contributor

@d-v-b@dcherian

  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

In this example the mode was explicitly set to "w" which will overwrite the data when opening a store. The default value of MemoryStore() is mode="r" which will raise a ValueError in this code snippet as it does not allow writing. I think, what the code snippet was trying to achieve was for reusing a closed array which can be done with mode "r+" or "a".
I'd say this illustrates that the effect of the AccessModeLiterals should be communicated better. Currently only the asynchronous open method contains docstrings explaining:

 mode : {'r', 'r+', 'a', 'w', 'w-'}, optional
Persistence mode: 'r' means read only (must exist); 'r+' means
read/write (must exist); 'a' means read/write (create if doesn't
exist); 'w' means create (overwrite if exists); 'w-' means create
(fail if exists).

Furthermore, the examples in the docstrings of array.py suggest using mode="w". I think overwriting stores on opening them has it's usecase but should not be communicated as a general good idea.

The other problem with implicitly opening a store is a problem, too. I agree with the comments above.

Edit:
Also, in store/core.py::make_store_path the mode will fallback to "w":

 elif store_like is None:
if mode is None:
mode = "w" # exception to the default mode = 'r'

I think it make sense here to fallback to "a", too

Comment threadsrc/zarr/store/memory.py Outdated
Co-authored-by: Hannes Spitz <44113112+brokkoli71@users.noreply.github.com>
@brokkoli71

Copy link
Copy Markdown
Contributor

I had a similar thought process while working on #2000. On the one hand sync(Store.open()) seems a bit much for the API, on the other hand implicitly calling _open might cause confusion. Currently the only modification of _open on the array is clearing it if overwrite is enabled. Maybe if we make that behavior more clear to the user that might not be a problem? what do you think? @d-v-b

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

@TomAugspurger do you have any ideas for how we could address this cleanly? for background, I am not happy with the solution I am pursuing in this PR, so we shouldn't give that much weight

@TomAugspurger

Copy link
Copy Markdown
Contributor

I don't have a strong opinion after thinking through this for a bit, but here's something:

I wonder if we have two different kinds of "opening" (and closing) going on here:

  1. A "resource"-level open / close: manage database connections, acquire locks, etc, which must be done before any operation can succeed.
  2. A logical open / close: do the stuff you need to do when you open (or close) a Store to match the semantics we want. Which I think is just clear non-empty stores with mode="w" and error on non-empty stores with mode="w-".

IMO, the logical stuff should just happen the very first time a store is created using Store.open. And I think that's the only time our current Store._open (which does the clearing for mode="w") should be called.

The resource-level stuff can happen whenever I think (perhaps with APIs for users to control).

This is all assuming that the Stores are created with MyStore.open, which maybe isn't reasonable (but we could force it). I dunno...

@jhamman
jhamman changed the base branch from v3 to mainOctober 14, 2024 20:58
@jhammanjhamman added this to the After 3.0.0 milestone Oct 17, 2024
@jhamman

Copy link
Copy Markdown
Member

@d-v-b - 75% sure this can be closed now that #2442 is in. What do you think?

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

i agree!

@d-v-bd-v-b closed this Nov 13, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants

@d-v-b@dcherian@brokkoli71@TomAugspurger@jhamman
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

data wiped from MemoryStore after store.close + array[:] - #2067

Closed
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close
Closed

data wiped from MemoryStore after store.close + array[:]#2067
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close

Conversation

@d-v-b

@d-v-bd-v-b commented Aug 6, 2024

Copy link
Copy Markdown
Contributor

Over in #1746@dcherian discovered the following bug:

importzarrfromzarr.arrayimportArrayfromzarr.groupimportGroupfromzarr.storeimportMemoryStoreimportnumpyasnpstore=MemoryStore(mode="w")
root=Group.create(store)
nparray=np.array([1], dtype=np.int8)
a=root.create_array(
"/0/0",
shape=nparray.shape,
chunks=(1,),
dtype=nparray.dtype.str,
attributes={},
# compressor=compressor, # TODO: FIXMEfill_value=nparray.dtype.type(0),
)
a[:] =nparrayprint(a[:]) # [1]store.close()
print(a[:]) # [0]

Setting data with a[:] = nparray, followed by a.store_path.store.close(), followed by a[:], results in the store getting wiped. This is not intended behavior.

this PR adds:

  • type hints to the indexing tests
  • a test for the aforementioned bug, which is currently failing, because we have not fixed it yet. this PR should be updated when we fix the bug.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

Closes#2067

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

so i figured out where this is coming from. i'm not sure if it's a bug or not.

  1. we close the store, which sets is_open to False.
  2. a[:] invokes MemoryStore.get(), which hits this line:
ifnotself._is_open:
awaitself._open()
  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

I don't fully understand the open / closed semantics here. My intuition is that re-opening a closed store feels weird, and having the store._open() routine invoked outside of Store.__init__ feels incorrect.

@brokkoli71 what's the logic for invoking Store._open in the get and set methods? I feel like it might be cleaner to only open the store in 1 place (__init__), and attempting to call Store.get/set on a closed store should simply error.

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

tests are now passing with this PR. Instead of conditionally calling store._open() in store.get or store.set, a RuntimeError is raised when attempting to use these methods on a store that is closed. I also adjusted all the tests to use sync(Store.open()) instead of Store().

I think we should not merge this until we have a conversation about the store design. I would really like to be able to create store classes in synchronous code without needing sync, but we can't do that now because (some) store initialization requires IO, which is async.

@jhamman and @normanrz any thoughts about this issue (i.e., that we cannot fully initialize a store class in synchronous code without sync)?

@dcherian

Copy link
Copy Markdown
Contributor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

The bug was that the store class self-destructs if a) it was created with overwrite=True, and b) you attempt to do any array indexing after closing the store.

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for working on this @d-v-b!

Comment threadsrc/zarr/store/memory.py Outdated
Comment on lines +73 to +74
if not self._is_open:
await self._open()
raise RuntimeError("Store is closed. Cannot `get` from a closed store.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

turn this into self._check_open() like self._check_writable()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also, should we propagate this to all other stores?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

both good ideas! the fact that tests passed without me making the last change means we need more tests for this

@jhammanjhamman added the V3 label Aug 9, 2024
@brokkoli71

brokkoli71 commented Aug 12, 2024

Copy link
Copy Markdown
Contributor

@d-v-b@dcherian

  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

In this example the mode was explicitly set to "w" which will overwrite the data when opening a store. The default value of MemoryStore() is mode="r" which will raise a ValueError in this code snippet as it does not allow writing. I think, what the code snippet was trying to achieve was for reusing a closed array which can be done with mode "r+" or "a".
I'd say this illustrates that the effect of the AccessModeLiterals should be communicated better. Currently only the asynchronous open method contains docstrings explaining:

 mode : {'r', 'r+', 'a', 'w', 'w-'}, optional
Persistence mode: 'r' means read only (must exist); 'r+' means
read/write (must exist); 'a' means read/write (create if doesn't
exist); 'w' means create (overwrite if exists); 'w-' means create
(fail if exists).

Furthermore, the examples in the docstrings of array.py suggest using mode="w". I think overwriting stores on opening them has it's usecase but should not be communicated as a general good idea.

The other problem with implicitly opening a store is a problem, too. I agree with the comments above.

Edit:
Also, in store/core.py::make_store_path the mode will fallback to "w":

 elif store_like is None:
if mode is None:
mode = "w" # exception to the default mode = 'r'

I think it make sense here to fallback to "a", too

Comment threadsrc/zarr/store/memory.py Outdated
Co-authored-by: Hannes Spitz <44113112+brokkoli71@users.noreply.github.com>
@brokkoli71

Copy link
Copy Markdown
Contributor

I had a similar thought process while working on #2000. On the one hand sync(Store.open()) seems a bit much for the API, on the other hand implicitly calling _open might cause confusion. Currently the only modification of _open on the array is clearing it if overwrite is enabled. Maybe if we make that behavior more clear to the user that might not be a problem? what do you think? @d-v-b

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

@TomAugspurger do you have any ideas for how we could address this cleanly? for background, I am not happy with the solution I am pursuing in this PR, so we shouldn't give that much weight

@TomAugspurger

Copy link
Copy Markdown
Contributor

I don't have a strong opinion after thinking through this for a bit, but here's something:

I wonder if we have two different kinds of "opening" (and closing) going on here:

  1. A "resource"-level open / close: manage database connections, acquire locks, etc, which must be done before any operation can succeed.
  2. A logical open / close: do the stuff you need to do when you open (or close) a Store to match the semantics we want. Which I think is just clear non-empty stores with mode="w" and error on non-empty stores with mode="w-".

IMO, the logical stuff should just happen the very first time a store is created using Store.open. And I think that's the only time our current Store._open (which does the clearing for mode="w") should be called.

The resource-level stuff can happen whenever I think (perhaps with APIs for users to control).

This is all assuming that the Stores are created with MyStore.open, which maybe isn't reasonable (but we could force it). I dunno...

@jhamman
jhamman changed the base branch from v3 to mainOctober 14, 2024 20:58
@jhammanjhamman added this to the After 3.0.0 milestone Oct 17, 2024
@jhamman

Copy link
Copy Markdown
Member

@d-v-b - 75% sure this can be closed now that #2442 is in. What do you think?

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

i agree!

@d-v-bd-v-b closed this Nov 13, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants

@d-v-b@dcherian@brokkoli71@TomAugspurger@jhamman
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

data wiped from MemoryStore after store.close + array[:] - #2067

Closed
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close
Closed

data wiped from MemoryStore after store.close + array[:]#2067
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close

Conversation

@d-v-b

@d-v-bd-v-b commented Aug 6, 2024

Copy link
Copy Markdown
Contributor

Over in #1746@dcherian discovered the following bug:

importzarrfromzarr.arrayimportArrayfromzarr.groupimportGroupfromzarr.storeimportMemoryStoreimportnumpyasnpstore=MemoryStore(mode="w")
root=Group.create(store)
nparray=np.array([1], dtype=np.int8)
a=root.create_array(
"/0/0",
shape=nparray.shape,
chunks=(1,),
dtype=nparray.dtype.str,
attributes={},
# compressor=compressor, # TODO: FIXMEfill_value=nparray.dtype.type(0),
)
a[:] =nparrayprint(a[:]) # [1]store.close()
print(a[:]) # [0]

Setting data with a[:] = nparray, followed by a.store_path.store.close(), followed by a[:], results in the store getting wiped. This is not intended behavior.

this PR adds:

  • type hints to the indexing tests
  • a test for the aforementioned bug, which is currently failing, because we have not fixed it yet. this PR should be updated when we fix the bug.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

Closes#2067

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

so i figured out where this is coming from. i'm not sure if it's a bug or not.

  1. we close the store, which sets is_open to False.
  2. a[:] invokes MemoryStore.get(), which hits this line:
ifnotself._is_open:
awaitself._open()
  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

I don't fully understand the open / closed semantics here. My intuition is that re-opening a closed store feels weird, and having the store._open() routine invoked outside of Store.__init__ feels incorrect.

@brokkoli71 what's the logic for invoking Store._open in the get and set methods? I feel like it might be cleaner to only open the store in 1 place (__init__), and attempting to call Store.get/set on a closed store should simply error.

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

tests are now passing with this PR. Instead of conditionally calling store._open() in store.get or store.set, a RuntimeError is raised when attempting to use these methods on a store that is closed. I also adjusted all the tests to use sync(Store.open()) instead of Store().

I think we should not merge this until we have a conversation about the store design. I would really like to be able to create store classes in synchronous code without needing sync, but we can't do that now because (some) store initialization requires IO, which is async.

@jhamman and @normanrz any thoughts about this issue (i.e., that we cannot fully initialize a store class in synchronous code without sync)?

@dcherian

Copy link
Copy Markdown
Contributor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

The bug was that the store class self-destructs if a) it was created with overwrite=True, and b) you attempt to do any array indexing after closing the store.

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for working on this @d-v-b!

Comment threadsrc/zarr/store/memory.py Outdated
Comment on lines +73 to +74
if not self._is_open:
await self._open()
raise RuntimeError("Store is closed. Cannot `get` from a closed store.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

turn this into self._check_open() like self._check_writable()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also, should we propagate this to all other stores?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

both good ideas! the fact that tests passed without me making the last change means we need more tests for this

@jhammanjhamman added the V3 label Aug 9, 2024
@brokkoli71

brokkoli71 commented Aug 12, 2024

Copy link
Copy Markdown
Contributor

@d-v-b@dcherian

  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

In this example the mode was explicitly set to "w" which will overwrite the data when opening a store. The default value of MemoryStore() is mode="r" which will raise a ValueError in this code snippet as it does not allow writing. I think, what the code snippet was trying to achieve was for reusing a closed array which can be done with mode "r+" or "a".
I'd say this illustrates that the effect of the AccessModeLiterals should be communicated better. Currently only the asynchronous open method contains docstrings explaining:

 mode : {'r', 'r+', 'a', 'w', 'w-'}, optional
Persistence mode: 'r' means read only (must exist); 'r+' means
read/write (must exist); 'a' means read/write (create if doesn't
exist); 'w' means create (overwrite if exists); 'w-' means create
(fail if exists).

Furthermore, the examples in the docstrings of array.py suggest using mode="w". I think overwriting stores on opening them has it's usecase but should not be communicated as a general good idea.

The other problem with implicitly opening a store is a problem, too. I agree with the comments above.

Edit:
Also, in store/core.py::make_store_path the mode will fallback to "w":

 elif store_like is None:
if mode is None:
mode = "w" # exception to the default mode = 'r'

I think it make sense here to fallback to "a", too

Comment threadsrc/zarr/store/memory.py Outdated
Co-authored-by: Hannes Spitz <44113112+brokkoli71@users.noreply.github.com>
@brokkoli71

Copy link
Copy Markdown
Contributor

I had a similar thought process while working on #2000. On the one hand sync(Store.open()) seems a bit much for the API, on the other hand implicitly calling _open might cause confusion. Currently the only modification of _open on the array is clearing it if overwrite is enabled. Maybe if we make that behavior more clear to the user that might not be a problem? what do you think? @d-v-b

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

@TomAugspurger do you have any ideas for how we could address this cleanly? for background, I am not happy with the solution I am pursuing in this PR, so we shouldn't give that much weight

@TomAugspurger

Copy link
Copy Markdown
Contributor

I don't have a strong opinion after thinking through this for a bit, but here's something:

I wonder if we have two different kinds of "opening" (and closing) going on here:

  1. A "resource"-level open / close: manage database connections, acquire locks, etc, which must be done before any operation can succeed.
  2. A logical open / close: do the stuff you need to do when you open (or close) a Store to match the semantics we want. Which I think is just clear non-empty stores with mode="w" and error on non-empty stores with mode="w-".

IMO, the logical stuff should just happen the very first time a store is created using Store.open. And I think that's the only time our current Store._open (which does the clearing for mode="w") should be called.

The resource-level stuff can happen whenever I think (perhaps with APIs for users to control).

This is all assuming that the Stores are created with MyStore.open, which maybe isn't reasonable (but we could force it). I dunno...

@jhamman
jhamman changed the base branch from v3 to mainOctober 14, 2024 20:58
@jhammanjhamman added this to the After 3.0.0 milestone Oct 17, 2024
@jhamman

Copy link
Copy Markdown
Member

@d-v-b - 75% sure this can be closed now that #2442 is in. What do you think?

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

i agree!

@d-v-bd-v-b closed this Nov 13, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants

@d-v-b@dcherian@brokkoli71@TomAugspurger@jhamman
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

data wiped from MemoryStore after store.close + array[:] - #2067

Closed
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close
Closed

data wiped from MemoryStore after store.close + array[:]#2067
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close

Conversation

@d-v-b

@d-v-bd-v-b commented Aug 6, 2024

Copy link
Copy Markdown
Contributor

Over in #1746@dcherian discovered the following bug:

importzarrfromzarr.arrayimportArrayfromzarr.groupimportGroupfromzarr.storeimportMemoryStoreimportnumpyasnpstore=MemoryStore(mode="w")
root=Group.create(store)
nparray=np.array([1], dtype=np.int8)
a=root.create_array(
"/0/0",
shape=nparray.shape,
chunks=(1,),
dtype=nparray.dtype.str,
attributes={},
# compressor=compressor, # TODO: FIXMEfill_value=nparray.dtype.type(0),
)
a[:] =nparrayprint(a[:]) # [1]store.close()
print(a[:]) # [0]

Setting data with a[:] = nparray, followed by a.store_path.store.close(), followed by a[:], results in the store getting wiped. This is not intended behavior.

this PR adds:

  • type hints to the indexing tests
  • a test for the aforementioned bug, which is currently failing, because we have not fixed it yet. this PR should be updated when we fix the bug.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

Closes#2067

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

so i figured out where this is coming from. i'm not sure if it's a bug or not.

  1. we close the store, which sets is_open to False.
  2. a[:] invokes MemoryStore.get(), which hits this line:
ifnotself._is_open:
awaitself._open()
  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

I don't fully understand the open / closed semantics here. My intuition is that re-opening a closed store feels weird, and having the store._open() routine invoked outside of Store.__init__ feels incorrect.

@brokkoli71 what's the logic for invoking Store._open in the get and set methods? I feel like it might be cleaner to only open the store in 1 place (__init__), and attempting to call Store.get/set on a closed store should simply error.

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

tests are now passing with this PR. Instead of conditionally calling store._open() in store.get or store.set, a RuntimeError is raised when attempting to use these methods on a store that is closed. I also adjusted all the tests to use sync(Store.open()) instead of Store().

I think we should not merge this until we have a conversation about the store design. I would really like to be able to create store classes in synchronous code without needing sync, but we can't do that now because (some) store initialization requires IO, which is async.

@jhamman and @normanrz any thoughts about this issue (i.e., that we cannot fully initialize a store class in synchronous code without sync)?

@dcherian

Copy link
Copy Markdown
Contributor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

The bug was that the store class self-destructs if a) it was created with overwrite=True, and b) you attempt to do any array indexing after closing the store.

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for working on this @d-v-b!

Comment threadsrc/zarr/store/memory.py Outdated
Comment on lines +73 to +74
if not self._is_open:
await self._open()
raise RuntimeError("Store is closed. Cannot `get` from a closed store.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

turn this into self._check_open() like self._check_writable()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also, should we propagate this to all other stores?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

both good ideas! the fact that tests passed without me making the last change means we need more tests for this

@jhammanjhamman added the V3 label Aug 9, 2024
@brokkoli71

brokkoli71 commented Aug 12, 2024

Copy link
Copy Markdown
Contributor

@d-v-b@dcherian

  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

In this example the mode was explicitly set to "w" which will overwrite the data when opening a store. The default value of MemoryStore() is mode="r" which will raise a ValueError in this code snippet as it does not allow writing. I think, what the code snippet was trying to achieve was for reusing a closed array which can be done with mode "r+" or "a".
I'd say this illustrates that the effect of the AccessModeLiterals should be communicated better. Currently only the asynchronous open method contains docstrings explaining:

 mode : {'r', 'r+', 'a', 'w', 'w-'}, optional
Persistence mode: 'r' means read only (must exist); 'r+' means
read/write (must exist); 'a' means read/write (create if doesn't
exist); 'w' means create (overwrite if exists); 'w-' means create
(fail if exists).

Furthermore, the examples in the docstrings of array.py suggest using mode="w". I think overwriting stores on opening them has it's usecase but should not be communicated as a general good idea.

The other problem with implicitly opening a store is a problem, too. I agree with the comments above.

Edit:
Also, in store/core.py::make_store_path the mode will fallback to "w":

 elif store_like is None:
if mode is None:
mode = "w" # exception to the default mode = 'r'

I think it make sense here to fallback to "a", too

Comment threadsrc/zarr/store/memory.py Outdated
Co-authored-by: Hannes Spitz <44113112+brokkoli71@users.noreply.github.com>
@brokkoli71

Copy link
Copy Markdown
Contributor

I had a similar thought process while working on #2000. On the one hand sync(Store.open()) seems a bit much for the API, on the other hand implicitly calling _open might cause confusion. Currently the only modification of _open on the array is clearing it if overwrite is enabled. Maybe if we make that behavior more clear to the user that might not be a problem? what do you think? @d-v-b

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

@TomAugspurger do you have any ideas for how we could address this cleanly? for background, I am not happy with the solution I am pursuing in this PR, so we shouldn't give that much weight

@TomAugspurger

Copy link
Copy Markdown
Contributor

I don't have a strong opinion after thinking through this for a bit, but here's something:

I wonder if we have two different kinds of "opening" (and closing) going on here:

  1. A "resource"-level open / close: manage database connections, acquire locks, etc, which must be done before any operation can succeed.
  2. A logical open / close: do the stuff you need to do when you open (or close) a Store to match the semantics we want. Which I think is just clear non-empty stores with mode="w" and error on non-empty stores with mode="w-".

IMO, the logical stuff should just happen the very first time a store is created using Store.open. And I think that's the only time our current Store._open (which does the clearing for mode="w") should be called.

The resource-level stuff can happen whenever I think (perhaps with APIs for users to control).

This is all assuming that the Stores are created with MyStore.open, which maybe isn't reasonable (but we could force it). I dunno...

@jhamman
jhamman changed the base branch from v3 to mainOctober 14, 2024 20:58
@jhammanjhamman added this to the After 3.0.0 milestone Oct 17, 2024
@jhamman

Copy link
Copy Markdown
Member

@d-v-b - 75% sure this can be closed now that #2442 is in. What do you think?

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

i agree!

@d-v-bd-v-b closed this Nov 13, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants

@d-v-b@dcherian@brokkoli71@TomAugspurger@jhamman
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

data wiped from MemoryStore after store.close + array[:] - #2067

Closed
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close
Closed

data wiped from MemoryStore after store.close + array[:]#2067
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close

Conversation

@d-v-b

@d-v-bd-v-b commented Aug 6, 2024

Copy link
Copy Markdown
Contributor

Over in #1746@dcherian discovered the following bug:

importzarrfromzarr.arrayimportArrayfromzarr.groupimportGroupfromzarr.storeimportMemoryStoreimportnumpyasnpstore=MemoryStore(mode="w")
root=Group.create(store)
nparray=np.array([1], dtype=np.int8)
a=root.create_array(
"/0/0",
shape=nparray.shape,
chunks=(1,),
dtype=nparray.dtype.str,
attributes={},
# compressor=compressor, # TODO: FIXMEfill_value=nparray.dtype.type(0),
)
a[:] =nparrayprint(a[:]) # [1]store.close()
print(a[:]) # [0]

Setting data with a[:] = nparray, followed by a.store_path.store.close(), followed by a[:], results in the store getting wiped. This is not intended behavior.

this PR adds:

  • type hints to the indexing tests
  • a test for the aforementioned bug, which is currently failing, because we have not fixed it yet. this PR should be updated when we fix the bug.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

Closes#2067

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

so i figured out where this is coming from. i'm not sure if it's a bug or not.

  1. we close the store, which sets is_open to False.
  2. a[:] invokes MemoryStore.get(), which hits this line:
ifnotself._is_open:
awaitself._open()
  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

I don't fully understand the open / closed semantics here. My intuition is that re-opening a closed store feels weird, and having the store._open() routine invoked outside of Store.__init__ feels incorrect.

@brokkoli71 what's the logic for invoking Store._open in the get and set methods? I feel like it might be cleaner to only open the store in 1 place (__init__), and attempting to call Store.get/set on a closed store should simply error.

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

tests are now passing with this PR. Instead of conditionally calling store._open() in store.get or store.set, a RuntimeError is raised when attempting to use these methods on a store that is closed. I also adjusted all the tests to use sync(Store.open()) instead of Store().

I think we should not merge this until we have a conversation about the store design. I would really like to be able to create store classes in synchronous code without needing sync, but we can't do that now because (some) store initialization requires IO, which is async.

@jhamman and @normanrz any thoughts about this issue (i.e., that we cannot fully initialize a store class in synchronous code without sync)?

@dcherian

Copy link
Copy Markdown
Contributor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

The bug was that the store class self-destructs if a) it was created with overwrite=True, and b) you attempt to do any array indexing after closing the store.

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for working on this @d-v-b!

Comment threadsrc/zarr/store/memory.py Outdated
Comment on lines +73 to +74
if not self._is_open:
await self._open()
raise RuntimeError("Store is closed. Cannot `get` from a closed store.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

turn this into self._check_open() like self._check_writable()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also, should we propagate this to all other stores?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

both good ideas! the fact that tests passed without me making the last change means we need more tests for this

@jhammanjhamman added the V3 label Aug 9, 2024
@brokkoli71

brokkoli71 commented Aug 12, 2024

Copy link
Copy Markdown
Contributor

@d-v-b@dcherian

  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

In this example the mode was explicitly set to "w" which will overwrite the data when opening a store. The default value of MemoryStore() is mode="r" which will raise a ValueError in this code snippet as it does not allow writing. I think, what the code snippet was trying to achieve was for reusing a closed array which can be done with mode "r+" or "a".
I'd say this illustrates that the effect of the AccessModeLiterals should be communicated better. Currently only the asynchronous open method contains docstrings explaining:

 mode : {'r', 'r+', 'a', 'w', 'w-'}, optional
Persistence mode: 'r' means read only (must exist); 'r+' means
read/write (must exist); 'a' means read/write (create if doesn't
exist); 'w' means create (overwrite if exists); 'w-' means create
(fail if exists).

Furthermore, the examples in the docstrings of array.py suggest using mode="w". I think overwriting stores on opening them has it's usecase but should not be communicated as a general good idea.

The other problem with implicitly opening a store is a problem, too. I agree with the comments above.

Edit:
Also, in store/core.py::make_store_path the mode will fallback to "w":

 elif store_like is None:
if mode is None:
mode = "w" # exception to the default mode = 'r'

I think it make sense here to fallback to "a", too

Comment threadsrc/zarr/store/memory.py Outdated
Co-authored-by: Hannes Spitz <44113112+brokkoli71@users.noreply.github.com>
@brokkoli71

Copy link
Copy Markdown
Contributor

I had a similar thought process while working on #2000. On the one hand sync(Store.open()) seems a bit much for the API, on the other hand implicitly calling _open might cause confusion. Currently the only modification of _open on the array is clearing it if overwrite is enabled. Maybe if we make that behavior more clear to the user that might not be a problem? what do you think? @d-v-b

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

@TomAugspurger do you have any ideas for how we could address this cleanly? for background, I am not happy with the solution I am pursuing in this PR, so we shouldn't give that much weight

@TomAugspurger

Copy link
Copy Markdown
Contributor

I don't have a strong opinion after thinking through this for a bit, but here's something:

I wonder if we have two different kinds of "opening" (and closing) going on here:

  1. A "resource"-level open / close: manage database connections, acquire locks, etc, which must be done before any operation can succeed.
  2. A logical open / close: do the stuff you need to do when you open (or close) a Store to match the semantics we want. Which I think is just clear non-empty stores with mode="w" and error on non-empty stores with mode="w-".

IMO, the logical stuff should just happen the very first time a store is created using Store.open. And I think that's the only time our current Store._open (which does the clearing for mode="w") should be called.

The resource-level stuff can happen whenever I think (perhaps with APIs for users to control).

This is all assuming that the Stores are created with MyStore.open, which maybe isn't reasonable (but we could force it). I dunno...

@jhamman
jhamman changed the base branch from v3 to mainOctober 14, 2024 20:58
@jhammanjhamman added this to the After 3.0.0 milestone Oct 17, 2024
@jhamman

Copy link
Copy Markdown
Member

@d-v-b - 75% sure this can be closed now that #2442 is in. What do you think?

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

i agree!

@d-v-bd-v-b closed this Nov 13, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants

@d-v-b@dcherian@brokkoli71@TomAugspurger@jhamman
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

data wiped from MemoryStore after store.close + array[:] - #2067

Closed
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close
Closed

data wiped from MemoryStore after store.close + array[:]#2067
d-v-b wants to merge 5 commits into
zarr-developers:mainfrom
d-v-b:indexing-after-close

Conversation

@d-v-b

@d-v-bd-v-b commented Aug 6, 2024

Copy link
Copy Markdown
Contributor

Over in #1746@dcherian discovered the following bug:

importzarrfromzarr.arrayimportArrayfromzarr.groupimportGroupfromzarr.storeimportMemoryStoreimportnumpyasnpstore=MemoryStore(mode="w")
root=Group.create(store)
nparray=np.array([1], dtype=np.int8)
a=root.create_array(
"/0/0",
shape=nparray.shape,
chunks=(1,),
dtype=nparray.dtype.str,
attributes={},
# compressor=compressor, # TODO: FIXMEfill_value=nparray.dtype.type(0),
)
a[:] =nparrayprint(a[:]) # [1]store.close()
print(a[:]) # [0]

Setting data with a[:] = nparray, followed by a.store_path.store.close(), followed by a[:], results in the store getting wiped. This is not intended behavior.

this PR adds:

  • type hints to the indexing tests
  • a test for the aforementioned bug, which is currently failing, because we have not fixed it yet. this PR should be updated when we fix the bug.

TODO:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/tutorial.rst
  • Changes documented in docs/release.rst
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

Closes#2067

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

so i figured out where this is coming from. i'm not sure if it's a bug or not.

  1. we close the store, which sets is_open to False.
  2. a[:] invokes MemoryStore.get(), which hits this line:
ifnotself._is_open:
awaitself._open()
  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

I don't fully understand the open / closed semantics here. My intuition is that re-opening a closed store feels weird, and having the store._open() routine invoked outside of Store.__init__ feels incorrect.

@brokkoli71 what's the logic for invoking Store._open in the get and set methods? I feel like it might be cleaner to only open the store in 1 place (__init__), and attempting to call Store.get/set on a closed store should simply error.

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

tests are now passing with this PR. Instead of conditionally calling store._open() in store.get or store.set, a RuntimeError is raised when attempting to use these methods on a store that is closed. I also adjusted all the tests to use sync(Store.open()) instead of Store().

I think we should not merge this until we have a conversation about the store design. I would really like to be able to create store classes in synchronous code without needing sync, but we can't do that now because (some) store initialization requires IO, which is async.

@jhamman and @normanrz any thoughts about this issue (i.e., that we cannot fully initialize a store class in synchronous code without sync)?

@dcherian

Copy link
Copy Markdown
Contributor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

@d-v-b

d-v-b commented Aug 6, 2024

Copy link
Copy Markdown
ContributorAuthor

So is the bug here thati was reusing the array object I wrote to? But instead I should create a new one?

The bug was that the store class self-destructs if a) it was created with overwrite=True, and b) you attempt to do any array indexing after closing the store.

@jhammanjhamman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for working on this @d-v-b!

Comment threadsrc/zarr/store/memory.py Outdated
Comment on lines +73 to +74
if not self._is_open:
await self._open()
raise RuntimeError("Store is closed. Cannot `get` from a closed store.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

turn this into self._check_open() like self._check_writable()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also, should we propagate this to all other stores?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

both good ideas! the fact that tests passed without me making the last change means we need more tests for this

@jhammanjhamman added the V3 label Aug 9, 2024
@brokkoli71

brokkoli71 commented Aug 12, 2024

Copy link
Copy Markdown
Contributor

@d-v-b@dcherian

  1. Store._open() does different things depending on its mode parameter. it seems that the default mode is AccessMode(readonly=False, overwrite=True, create=True, update=False). Because overwrite is True here, Store._openwipes the store before fetching data.

In this example the mode was explicitly set to "w" which will overwrite the data when opening a store. The default value of MemoryStore() is mode="r" which will raise a ValueError in this code snippet as it does not allow writing. I think, what the code snippet was trying to achieve was for reusing a closed array which can be done with mode "r+" or "a".
I'd say this illustrates that the effect of the AccessModeLiterals should be communicated better. Currently only the asynchronous open method contains docstrings explaining:

 mode : {'r', 'r+', 'a', 'w', 'w-'}, optional
Persistence mode: 'r' means read only (must exist); 'r+' means
read/write (must exist); 'a' means read/write (create if doesn't
exist); 'w' means create (overwrite if exists); 'w-' means create
(fail if exists).

Furthermore, the examples in the docstrings of array.py suggest using mode="w". I think overwriting stores on opening them has it's usecase but should not be communicated as a general good idea.

The other problem with implicitly opening a store is a problem, too. I agree with the comments above.

Edit:
Also, in store/core.py::make_store_path the mode will fallback to "w":

 elif store_like is None:
if mode is None:
mode = "w" # exception to the default mode = 'r'

I think it make sense here to fallback to "a", too

Comment threadsrc/zarr/store/memory.py Outdated
Co-authored-by: Hannes Spitz <44113112+brokkoli71@users.noreply.github.com>
@brokkoli71

Copy link
Copy Markdown
Contributor

I had a similar thought process while working on #2000. On the one hand sync(Store.open()) seems a bit much for the API, on the other hand implicitly calling _open might cause confusion. Currently the only modification of _open on the array is clearing it if overwrite is enabled. Maybe if we make that behavior more clear to the user that might not be a problem? what do you think? @d-v-b

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

@TomAugspurger do you have any ideas for how we could address this cleanly? for background, I am not happy with the solution I am pursuing in this PR, so we shouldn't give that much weight

@TomAugspurger

Copy link
Copy Markdown
Contributor

I don't have a strong opinion after thinking through this for a bit, but here's something:

I wonder if we have two different kinds of "opening" (and closing) going on here:

  1. A "resource"-level open / close: manage database connections, acquire locks, etc, which must be done before any operation can succeed.
  2. A logical open / close: do the stuff you need to do when you open (or close) a Store to match the semantics we want. Which I think is just clear non-empty stores with mode="w" and error on non-empty stores with mode="w-".

IMO, the logical stuff should just happen the very first time a store is created using Store.open. And I think that's the only time our current Store._open (which does the clearing for mode="w") should be called.

The resource-level stuff can happen whenever I think (perhaps with APIs for users to control).

This is all assuming that the Stores are created with MyStore.open, which maybe isn't reasonable (but we could force it). I dunno...

@jhamman
jhamman changed the base branch from v3 to mainOctober 14, 2024 20:58
@jhammanjhamman added this to the After 3.0.0 milestone Oct 17, 2024
@jhamman

Copy link
Copy Markdown
Member

@d-v-b - 75% sure this can be closed now that #2442 is in. What do you think?

@d-v-b

Copy link
Copy Markdown
ContributorAuthor

i agree!

@d-v-bd-v-b closed this Nov 13, 2024
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants

@d-v-b@dcherian@brokkoli71@TomAugspurger@jhamman