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

feat(sql): disk spill support for FastMap #3877

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

hilmialf
Copy link

@hilmialf hilmialf commented Oct 20, 2023

WIP for #3848
Not ready for review, but created PR early for feedback

@CLAassistant
Copy link

CLAassistant commented Oct 20, 2023

CLA assistant check
All committers have signed the CLA.

@hilmialf
Copy link
Author

hilmialf commented Oct 20, 2023

@puzpuzpuz Hi Andrei, I am trying to switch implementation to use MemoryCARWImpl first before adding mechanism to switch to mmap version.

It currently still has memory leak since I do not check for maxResize and MAX_HEAP_SIZE limit yet.
If the current PR state is in the good direction, I'll proceed as follows:

  1. Add additional private vars inside MemoryCARWImpl (nResize, MAX_HEAP_SIZE) and perform check before each resize to obey the new limitation.
  2. Continue the work on implementing the mmap part.

looking forward to your feedback.

*update:
I tried imposing limit (nResize, MAX_HEAP_SIZE) but MemoryCARWImpl does not seem to extend the same way as original FastMap impl (<<1 for every extend).

@hilmialf hilmialf force-pushed the master branch 2 times, most recently from cccc77d to 9ba338e Compare October 20, 2023 13:41
@puzpuzpuz
Copy link
Contributor

  1. Add additional private vars inside MemoryCARWImpl (nResize, MAX_HEAP_SIZE) and perform check before each resize to obey the new limitation.

I'd avoid modifying MemoryCARWImpl - this class already has sufficient general-purpose API. The checks can be made externally, i.e. in FastMap.

but MemoryCARWImpl does not seem to extend the same way as original FastMap impl (<<1 for every extend).

That's also fine - as a user, I wouldn't be happy if the FastMap file grows from 4GB to 8GB in one go while it really needs 5GB. FastMap should employ different growth strategy based on the heap type (file-baked or not).

@hilmialf
Copy link
Author

@puzpuzpuz could you take a quick look on current PR state?
Here is what's changed:

  • pageSize is renamed to heapSize inside FastMap to make the name clearer.
  • created MemoryCARWSpillable along with unit tests. *let me revisit for the thoroughness, right now I just copied directly from MemoryCMARImplTest.

I would like your suggestion on the followings:

  • what should be the best default pageSize for CARWImpl inside MemoryCARWSpillable
  • similarly, what should be the best default pageSize for CMARWImpl inside MemoryCARWSpillable
  • Also, I wonder whether we need to make enforcement if both pageSize should be equal or not

In addition, I found several parts where (to me, CMIIW) looks inconsistent. I marked them with // TODO.
for instance:

  • extendedSegmentSize definition.

Copy link
Contributor

@puzpuzpuz puzpuzpuz left a comment

Choose a reason for hiding this comment

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

The overall direction looks good. Left a few comments here and there.

core/src/main/java/io/questdb/cairo/vm/api/MemoryCARW.java Outdated Show resolved Hide resolved

import static org.junit.Assert.*;

public class MemoryCARWSpillableTest extends AbstractCairoTest {
Copy link
Contributor

Choose a reason for hiding this comment

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

There should be some tests that assert the disk spill case (the file gets created when the memory grows and the memory contents are retained, the file gets deleted when the memory shrinks and so on). Also, some negative case tests would be great, e.g. if file creation fails, a meaningful exception is thrown; if file deletion fails on map shrink, an exception is thrown (yet, FastMap may ignore it in this scenario as we need to delete the temp dir on startup).

private String tmpFile;

// TODO: For now assume the same pageSize
public MemoryCARWSpillable(long pageSize, int maxPages, long diskSpillThreshold, CairoConfiguration cairoConf){
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to have so many constructors. It's enough to keep only the one needed by the FastMap. The same applies to all methods here - they can be FastMap-specific and there is no need to keep them if they're unused. In fact, this call doesn't even have to implement MemoryCARW especially considering that certain methods lose their meaning due to different underlying memories, e.g. getExtendSegmentSize(), getPageCount() or getPageSize().

Copy link
Author

Choose a reason for hiding this comment

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

I see..
Tbh, I found several inconsistencies inside MemoryCMARWImpl and MemoryCARWImpl where it might refer to different things (but most of the methods there such as getExtendSegmentSize() are derived from shared interface, so I believe they should convey the same meaning)

Copy link
Author

Choose a reason for hiding this comment

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

Btw, how should I set the pageSize for each memory type (filebased and plain mem)?
Is it configurable?
if so, what should be sensible default? 4kb?

Copy link
Contributor

Choose a reason for hiding this comment

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

For the "normal" (non-file-baked) memory, the page size should be the same as FastMap's initial size. For the mmapped memory, page size doesn't matter - the extend segment size does. I'd make both of these configurable, so that FastMap passes them when initializing the heap object. Also, the extend segment size should be configurable through the CairoConfiguration. The default value may be something like 16MB since we know that disk spill has to deal with large files and frequent file extensions will be costly with 4KB.

@hilmialf
Copy link
Author

Hi Andrei @puzpuzpuz. My apology for late follow-up 🙇

I updated in several places. Here is highlight from the last time:

  1. Added necessary Configuration
  2. Updated places where FastMap is instantiated to use the new constructor.

Left to do:

  1. Add test cases for spillable FastMap. Notes:
  • I did extensive testcase for SpillableHeapMemory in SpillableHeapMemoryTest. However, I have not done test yet from FastMapTest. Instead, I keep the current FastMapTest test case as it is with old FastMap constructor (unspillable memory).

I would like to hear your opinion, especially about the current config and settings.
In addition, I found the behavior of extend() for MemoryCARWImpl and MemoryCMARWImpl are different internally when the new size is multiple of pageSize. In current implementation, I somehow modify them so that they behave similarly. You can find the detail on the SpillableHeapMemory checkAndExtend() part. Please let me know if this is not what you thought it should be.

Thanks a lot for your time.

P.S.
Oops my formatter is unbehaving.. its somehow restructuring in unnecessary part of the code. Could you point out where I setup wrong with the formatter? I tried to follow the development guide.

@puzpuzpuz puzpuzpuz added the New feature Feature requests label Nov 13, 2023
@puzpuzpuz
Copy link
Contributor

Oops my formatter is unbehaving.. its somehow restructuring in unnecessary part of the code. Could you point out where I setup wrong with the formatter? I tried to follow the development guide.

If you're using IntelliJ IDEA, it should pick up the code style automatically and it's checked in the repo. Personally I use the following actions on save, so that the edited files are always auto-formatted.
Screenshot from 2023-11-13 11-49-14

return;
}

// MemoryCMARWImpl does not have memory breached checking, so adding one here. In addition, default
Copy link
Contributor

Choose a reason for hiding this comment

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

The growth policy doesn't have to be consistent in both spilled and non-spilled modes: in the in-memory mode it should grow as a power of two (just like it does currently in the master branch), while in file-based mode it should grow in multiples of extendSegmentSize.

Copy link
Author

Choose a reason for hiding this comment

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

Hi Andrei,
Sorry for late follow up.
Id like to clarify more detail about the extend behavior since Ive been overthinking about this. To start, this is my current understanding:

  • pageSize should be power of two
  • in the in-mem mode, it grows as a power of two.
  • extendSegmentSize should also be power of two.
  • when it grows, it grows as a multiple of extendSegmentSize (thus the size of mmapped heap is not necessarily power of two)

Then,

  • In FastMap, the MAX_HEAP_SIZE is defined as (Integer.toUnsignedLong(-1) - 1) << 3. However, it is not a power of 2 (which might also not an exact multiples of extendSegmentSize). How do you think the extend behavior should be when extending up to MAX_HEAP_SIZE?
    Is it okay to allow memory to extend beyond the MAX_HEAP_SIZE? And if it is allowed, is special handling required so that only part of the allocated memory can be used instead of all (in this case, we only use the memory up to MAX_HEAP_SIZE rather than its actual allocated size)

Copy link
Author

@hilmialf hilmialf Nov 25, 2023

Choose a reason for hiding this comment

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

In addition, lets say the requested size is n * pageSize, what should be the allocated size?
In current implementation, CARW and CMARW has different implementation.

  • CARW: requested n * pageSize -> given n * pageSize
  • CMARW: requested n * pageSize -> given (n+1) * pageSize

Copy link
Contributor

Choose a reason for hiding this comment

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

  • However, it is not a power of 2 (which might also not an exact multiples of extendSegmentSize). How do you think the extend behavior should be when extending up to MAX_HEAP_SIZE?

MAX_HEAP_SIZE is the max possible capacity of the heap, i.e. it's a threshold, so it doesn't have to be a power of 2. We use it to check if the heap capacity after growth would exceed the threshold, not to initialize the heap.

  • Is it okay to allow memory to extend beyond the MAX_HEAP_SIZE?

No, this shouldn't be allowed considering that we won't be able to index entries beyond MAX_HEAP_SIZE. It's better to fail fast when we detect such situation when growing the heap.

Copy link
Contributor

Choose a reason for hiding this comment

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

In addition, lets say the requested size is n * pageSize, what should be the allocated size?

In file-based mode, we should grow the file in pages (page size should be multiple of the OS page, see Files.PAGE_SIZE). In in-memory mode, we should keep existing logic with the power of 2. In both cases, the allocated size should be equal or larger than the requested size.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the clarification. So, lets say the extendSegmentSize is set to 4GB (default value you proposed earlier) and the current heap size is 28GB (7 pages). When map is required to grow to 29GB, for example, since it extends based on page size, it will never be allowed since the next target size is 32GB = 34,359,738,368, while the max allowed heap is Integer.toUnsignedLong(-1) - 1) << 3 = 34,359,738,352.
In that case, is it okay to just throw error, or should it be allowed to grow to the MAX_HEAP_SIZE?

Copy link
Contributor

Choose a reason for hiding this comment

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

In that case, is it okay to just throw error, or should it be allowed to grow to the MAX_HEAP_SIZE?

I'd say that if the requested size (29GB) is under MAX_HEAP_SIZE, we should grow to MAX_HEAP_SIZE. After that, the next growth attempt should fail.

So, lets say the extendSegmentSize is set to 4GB (default value you proposed earlier)

I recall that I mentioned 4GB as the disk spill threshold, not the extendSegmentSize. I'd make extendSegmentSize something in the 256-512MB range by default. The idea is to do less costly file growth operations, while not growing it too rapidly to avoid using the disk space too eagerly.

Copy link
Author

Choose a reason for hiding this comment

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

I recall that I mentioned 4GB as the disk spill threshold, not the extendSegmentSize. I'd make extendSegmentSize something in the 256-512MB range by default. The idea is to do less costly file growth operations, while not growing it too rapidly to avoid using the disk space too eagerly.

I see

I'd say that if the requested size (29GB) is under MAX_HEAP_SIZE, we should grow to MAX_HEAP_SIZE. After that, the next growth attempt should fail.

In summary, does that mean the last extension may extend without respecting the extendSegmentSize?
Or in other word, should we extend up to MAX_HEAP_SIZE, or up to the size below MAX_HEAP_SIZE which is multiple of extendSegmentSize?

Copy link
Contributor

Choose a reason for hiding this comment

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

In summary, does that mean the last extension may extend without respecting the extendSegmentSize?
Or in other word, should we extend up to MAX_HEAP_SIZE, or up to the size below MAX_HEAP_SIZE which is multiple of extendSegmentSize?

Yes, that sounds logical to me. File extension should try to use as much heap space as possible.


public class SpillableHeapMemoryTest extends AbstractCairoTest {
private static FilesFacade ff = new FilesFacadeImpl();
private static Path spillPath = Path.getThreadLocal(root).concat("fastmap.tmp").$();
Copy link
Contributor

Choose a reason for hiding this comment

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

Thread-local paths should be used in-place, without storing them in a field. Otherwise, chances are high that they'll get mutated or released.

Copy link
Author

Choose a reason for hiding this comment

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

I believe it also applies similarly when passing it down to class instantiation as an argument?

Copy link
Contributor

Choose a reason for hiding this comment

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

If the class constructor doesn't store the path as a field, but uses it locally, that's fine. We have @Transient annotation to mark arguments that shouldn't outlive the constructor call.

import static org.junit.Assert.*;

public class SpillableHeapMemoryTest extends AbstractCairoTest {
private static FilesFacade ff = new FilesFacadeImpl();
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be a good idea to add some tests around file errors, e.g. what happens when a disk spill fails since there is no disk space and so on. Check other tests that define an anonymous sub-class for TestFilesFacadeImpl.

@puzpuzpuz
Copy link
Contributor

In addition, I found the behavior of extend() for MemoryCARWImpl and MemoryCMARWImpl are different internally when the new size is multiple of pageSize. In current implementation, I somehow modify them so that they behave similarly. You can find the detail on the SpillableHeapMemory checkAndExtend() part. Please let me know if this is not what you thought it should be.

Left a few comments, including the one on the extension behavior.

@hilmialf
Copy link
Author

@puzpuzpuz Thanks a lot for taking your time to give a thorough review. That is really helpful for me. Will get back to you soon.
Additionally, I notice that the memory layout for FastMap has changed. Should I incorporate those changes to this PR?

@puzpuzpuz
Copy link
Contributor

Additionally, I notice that the memory layout for FastMap has changed. Should I incorporate those changes to this PR?

Yes, there were some recent changes around FastMap, sorry for that. I'm afraid you'd have to resolve conflicts. Please let me know if you need any guidance with that.

@puzpuzpuz puzpuzpuz changed the title disk spill support for FastMap feat(sql): disk spill support for FastMap Nov 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
New feature Feature requests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants