Skip to content

HPCC-33806 Add support of building indexes to dafilesrv#20197

Open
jpmcmu wants to merge 2 commits intohpcc-systems:candidate-9.14.xfrom
jpmcmu:HPCC-33806
Open

HPCC-33806 Add support of building indexes to dafilesrv#20197
jpmcmu wants to merge 2 commits intohpcc-systems:candidate-9.14.xfrom
jpmcmu:HPCC-33806

Conversation

@jpmcmu
Copy link
Contributor

@jpmcmu jpmcmu commented Jul 23, 2025

Type of change:

  • This change is a bug fix (non-breaking change which fixes an issue).
  • This change is a new feature (non-breaking change which adds functionality).
  • This change improves the code (refactor or other change that does not change the functionality)
  • This change fixes warnings (the fix does not alter the functionality or the generated code)
  • This change is a breaking change (fix or feature that will cause existing behavior to change).
  • This change alters the query API (existing queries will have to be recompiled)

Checklist:

  • My code follows the code style of this project.
    • My code does not create any new warnings from compiler, build system, or lint.
  • The commit message is properly formatted and free of typos.
    • The commit message title makes sense in a changelog, by itself.
    • The commit is signed.
  • My change requires a change to the documentation.
    • I have updated the documentation accordingly, or...
    • I have created a JIRA ticket to update the documentation.
    • Any new interfaces or exported functions are appropriately commented.
  • I have read the CONTRIBUTORS document.
  • The change has been fully tested:
    • I have added tests to cover my changes.
    • All new and existing tests passed.
    • I have checked that this change does not introduce memory leaks.
    • I have used Valgrind or similar tools to check for potential issues.
  • I have given due consideration to all of the following potential concerns:
    • Scalability
    • Performance
    • Security
    • Thread-safety
    • Cloud-compatibility
    • Premature optimization
    • Existing deployed queries will not be broken
    • This change fixes the problem, not just the symptom
    • The target branch of this pull request is appropriate for such a change.
  • There are no similar instances of the same problem that should be addressed
    • I have addressed them here
    • I have raised JIRA issues to address them separately
  • This is a user interface / front-end modification
    • I have tested my changes in multiple modern browsers
    • The component(s) render as expected

Smoketest:

  • Send notifications about my Pull Request position in Smoketest queue.
  • Test my draft Pull Request.

Testing:

@github-actions
Copy link

Jira Issue: https://hpccsystems.atlassian.net//browse/HPCC-33806

Jirabot Action Result:
Workflow Transition To: Merge Pending
Updated PR

if (compression == "default")
{
flags |= HTREE_COMPRESSED_KEY;
compression = "";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a better way to determine the "default" compression format?

Copy link
Member

Choose a reason for hiding this comment

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

this code should probably use translateToCompMethod(compression)

Copy link
Member

Choose a reason for hiding this comment

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

Not really - it is only a very small subset of compression types.

Copy link
Member

Choose a reason for hiding this comment

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

Realistically I think you should always set htree_compressed_key and then pass through the compression as is. Row compression is not used outside the regression suite.

I would change the check in keybuild.cpp:

if (!isEmptyString(compression))

to

if (!isEmptyString(compression) && !strsame(compression, "lzw") && && !strsame(compression, "default"))

Which will allow lzw to be explicitly defined if we ever change the default.

helper->setIndexMeta("_nodeSize", std::to_string(nodeSize));
}

if (config.hasProp("noSeek"))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does it make sense to expose these options? I tried to match was exposed to ECL

Copy link
Member

Choose a reason for hiding this comment

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

I think this should be defaulting to true - it should be true for blob storage, and doesn't really harm to be true for other systems.


inline void processRow(const void *row, uint64_t rowSize)
{
unsigned __int64 fpos = 0;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Setting the fpos correctly here is a bit odd, this would definitely need to come from the incoming record, but an fpos may not always make sense, and because the datasets are often projected it isn't easy to reliable get the fpos of a read dataset.

Copy link
Member

Choose a reason for hiding this comment

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

fpos only applicable if building an index of a base dataset (where fpos' refer to offset in flat file).
Not sure there's any need to support it.

Copy link
Member

Choose a reason for hiding this comment

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

Unfortunately ECL has weird semantics to reuse the fileposition field if the last field in the payload is numeric.

I think this is ok as it is, but the index will only be generally readable if it is defined with the FILEPOSITION(FALSE) attribute (if the last field is a numeric value).

If you want to be able to create all keys then you will need to do some horrible transformations to read the integer value of the last field, put it into the fileposition field.
Again create a separate jira to revisit.

@jpmcmu jpmcmu requested review from ghalliday and jakesmith July 23, 2025 13:18
@jpmcmu
Copy link
Contributor Author

jpmcmu commented Jul 23, 2025

@ghalliday @jakesmith Still working on a few things here especially in relation to the TLK / publishing, but writing of an index is working.

virtual void write(size32_t sz, const void *rowData) override
{
size32_t rowOffset = 0;
while(rowOffset < sz)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Need to handle partial records here.

Copy link
Member

Choose a reason for hiding this comment

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

I think it should be illegal to write partial records to this function. Otherwise you have some notable complications - the call to find the row size needs protecting if the row is partial.
For the moment throw an error if rowOffset > sz

Copy link
Member

@jakesmith jakesmith left a comment

Choose a reason for hiding this comment

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

@jpmcmu - looks good in general. Please see comments.

}
};


Copy link
Member

Choose a reason for hiding this comment

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

trivial: leave one line, consistent with other spacing between classes.

translator.setown(createRecordTranslator(outRecord, inRecord));
}

virtual bool getIndexMeta(size32_t & lenName, char * & name, size32_t & lenValue, char * & value, unsigned idx)
Copy link
Member

Choose a reason for hiding this comment

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

trivial: add 'override'

if (config.hasProp("noSeek"))
{
bool noSeek = config.getPropBool("noSeek");
helper->setIndexMeta("_noSeek", noSeek ? "true" : "false");
Copy link
Member

Choose a reason for hiding this comment

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

trivial: can use boolToStr

return true;
}

void setIndexMeta(const std::string& name, const std::string& value)
Copy link
Member

Choose a reason for hiding this comment

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

picky: nicer if virtuals of IHThorIndexWriteArg kept together.

Copy link
Member

Choose a reason for hiding this comment

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

doesn't getWidth() need to be implemented with count of meta fields for getIndexMeta to be callable ?

if (idx >= indexMetaData.size())
return false;

auto it = indexMetaData.begin();
Copy link
Member

Choose a reason for hiding this comment

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

would a std::vector of a pair of std::string's be more suitable?


~CRemoteIndexWriteActivity()
{
if (builder != nullptr && helper != nullptr)
Copy link
Member

Choose a reason for hiding this comment

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

there is no alternative at the moment, but when the file is closed (StreamCmd::CLOSE), it should call through to the acitivity to close, so we don't depend on dtor's to do this kind of work.

For now, it would be worth aadding a try/catch - as any unhandled exception at this point (within a dtor) will cause the process to exit.

Copy link
Member

Choose a reason for hiding this comment

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

Related to this, I think you are going to need to serialize back the last row, so that when the client has finished writing all parts of an index, it can use those last parts to create the TLK.
The response from StreamCmd::CLOSE could be extended to return structured info, that container this serialize row data.

if (builder != nullptr && helper != nullptr)
{
Owned<IPropertyTree> metadata;
metadata.setown(createPTree("metadata", ipt_fast));
Copy link
Member

Choose a reason for hiding this comment

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

trivial: ^ could be on 1 line : Owned metadata = createPTree("metadata");

not worth diverging away from default to specify ipt_fast in this case, it's the default anyway.

size32_t rowOffset = 0;
while(rowOffset < sz)
{
const RtlRecord& inputRecordAccessor = inMeta->queryRecordAccessor(true);
Copy link
Member

Choose a reason for hiding this comment

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

could be done once and stored as member.

}

extern jlib_decl void toLower(std::string & value);
extern jlib_decl void trim(std::string & value);
Copy link
Member

Choose a reason for hiding this comment

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

could do with comment.. what does it do? Looks like trims leading white space only not trailing


std::string compression = config.queryProp("compressed", "default");
toLower(compression);
trim(compression);
Copy link
Member

Choose a reason for hiding this comment

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

curious why this string might need leading spaces trimmed ? (vs any other string)

Copy link
Member

Choose a reason for hiding this comment

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

I agree, I'm not sure why you would trim this field.

@ghalliday ghalliday marked this pull request as ready for review July 29, 2025 11:25
Copilot AI review requested due to automatic review settings July 29, 2025 11:25
@ghalliday
Copy link
Member

Converting to non draft - since ready to review and to allow copilot to run.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds support for building indexes to the dafilesrv (data file server) component, enabling remote index creation capabilities as part of HPCC-33806. The implementation includes a new index write activity class and supporting infrastructure.

  • Adds a new CRemoteIndexWriteActivity class to handle remote index building operations
  • Implements helper classes and utilities for index metadata management and record transformation
  • Adds configuration support for index compression, node size, and other index-specific options

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
system/jlib/jstring.hpp Adds declaration for new trim utility function
system/jlib/jstring.cpp Implements trim function for string whitespace removal
fs/dafsserver/dafsserver.cpp Adds complete index writing functionality with new classes and TAKindexwrite support
esp/services/ws_dfu/ws_dfuService.cpp Adds missing break statement and index file descriptor configuration

{
value.erase(value.begin(), std::find_if(value.begin(), value.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
Copy link

Copilot AI Jul 29, 2025

Choose a reason for hiding this comment

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

The trim function only removes leading whitespace but not trailing whitespace. A complete trim implementation should remove both leading and trailing whitespace. Consider using value.erase(std::find_if(value.rbegin(), value.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), value.end()) to also remove trailing whitespace.

Suggested change
}));
}));
value.erase(std::find_if(value.rbegin(), value.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), value.end());

Copilot uses AI. Check for mistakes.
size32_t indexRowSize = helper->transform(rowBuilder, row, this, fpos);

// Key builder checks for duplicate records so we can just check for sortedness
if (memcmp(prevRowBuffer.get(), rowBuffer.get(), helper->getKeyedSize()) > 0)
Copy link

Copilot AI Jul 29, 2025

Choose a reason for hiding this comment

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

The comparison uses memcmp which performs byte-wise comparison, but this may not be correct for all data types. For complex types like strings with different encodings or numeric types with different byte representations, this could give incorrect sort order results. Consider using a proper record comparison function that understands the data types.

Copilot uses AI. Check for mistakes.
void openFileStream()
{
if (!recursiveCreateDirectoryForFile(fileName))
throw createDafsExceptionV(DAFSERR_cmdstream_openfailure, "Failed to create dirtory for file: '%s'", fileName.get());
Copy link

Copilot AI Jul 29, 2025

Choose a reason for hiding this comment

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

Typo in error message: 'dirtory' should be 'directory'.

Suggested change
throw createDafsExceptionV(DAFSERR_cmdstream_openfailure, "Failed to create dirtory for file: '%s'", fileName.get());
throw createDafsExceptionV(DAFSERR_cmdstream_openfailure, "Failed to create directory for file: '%s'", fileName.get());

Copilot uses AI. Check for mistakes.
throw MakeStringException(99, "Index maximum record length (%d) exceeds 32k internal limit", maxDiskRecordSize);

rowBuffer.allocateN(maxDiskRecordSize, true);
prevRowBuffer.allocateN(maxDiskRecordSize, true);
Copy link

Copilot AI Jul 29, 2025

Choose a reason for hiding this comment

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

The prevRowBuffer is allocated with the full maxDiskRecordSize but only helper->getKeyedSize() bytes are used in the comparison. Consider allocating only the needed size for the keyed portion to reduce memory usage, especially for records with large non-keyed portions.

Suggested change
prevRowBuffer.allocateN(maxDiskRecordSize, true);
prevRowBuffer.allocateN(helper->getKeyedSize(), true);

Copilot uses AI. Check for mistakes.
}

~CRemoteIndexWriteActivity()
{
Copy link

Copilot AI Jul 29, 2025

Choose a reason for hiding this comment

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

The destructor performs complex operations including calling builder->finish() which could potentially throw exceptions. Destructors should not throw exceptions as this can lead to undefined behavior. Consider moving the finish() logic to a separate cleanup method that can be called explicitly before destruction.

Suggested change
{
{
try
{
cleanup();
}
catch (...)
{
// Log the exception or handle it appropriately
// Avoid propagating exceptions from the destructor
}
close();
}
void cleanup()
{

Copilot uses AI. Check for mistakes.
Copy link
Member

@ghalliday ghalliday left a comment

Choose a reason for hiding this comment

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

@jpmcmu This looks broadly right, but various comments.

virtual unsigned getFlags() { return flags; }
virtual size32_t transform(ARowBuilder & rowBuilder, const void * row, IBlobCreator * blobs, unsigned __int64 & filepos)
{
// Seems like an UnexpectedVirtualFieldCallback could be used but what about blobs?
Copy link
Member

Choose a reason for hiding this comment

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

Please create a separate jira for supporting blobs. It will need changes to the translator, including a new virtual in the callback interface.


inline void processRow(const void *row, uint64_t rowSize)
{
unsigned __int64 fpos = 0;
Copy link
Member

Choose a reason for hiding this comment

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

Unfortunately ECL has weird semantics to reuse the fileposition field if the last field in the payload is numeric.

I think this is ok as it is, but the index will only be generally readable if it is defined with the FILEPOSITION(FALSE) attribute (if the last field is a numeric value).

If you want to be able to create all keys then you will need to do some horrible transformations to read the integer value of the last field, put it into the fileposition field.
Again create a separate jira to revisit.

maxRecordSizeSeen = indexRowSize;

processed++;
memcpy(prevRowBuffer.get(), rowBuffer.get(), maxDiskRecordSize);
Copy link
Member

Choose a reason for hiding this comment

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

Only need to save keyedSize. I suspect benefit of catching invalid input data outweighs the cost.

size32_t indexRowSize = helper->transform(rowBuilder, row, this, fpos);

// Key builder checks for duplicate records so we can just check for sortedness
if (memcmp(prevRowBuffer.get(), rowBuffer.get(), helper->getKeyedSize()) > 0)
Copy link
Member

Choose a reason for hiding this comment

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

cache helper->getKeyedSize() in a member variable.

flags &= ~USE_TRAILING_HEADER;
}

size32_t fileposSize = hasTrailingFileposition(helper->queryDiskRecordSize()->queryTypeInfo()) ? sizeof(offset_t) : 0;
Copy link
Member

Choose a reason for hiding this comment

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

Throw an error if it has a trailing fileposition - will require changes elsewhere.

}
};

class CRemoteIndexWriteHelper : public CThorIndexWriteArg
Copy link
Member

Choose a reason for hiding this comment

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

Does this class actually provided any benefit? It is legal to call createKeyBuilder() with null for the helper.
I suspect it adds complication with no benefit.
I don't think there is currently a way of adding bloom filters without a helper, but that it would be better to add virtuals to allow that, and apply the values directly from a property tree.

Long term that is the direction that disk write is going for many options.

virtual void write(size32_t sz, const void *rowData) override
{
size32_t rowOffset = 0;
while(rowOffset < sz)
Copy link
Member

Choose a reason for hiding this comment

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

I think it should be illegal to write partial records to this function. Otherwise you have some notable complications - the call to find the row size needs protecting if the row is partial.
For the moment throw an error if rowOffset > sz

unsigned nodeSize = NODESIZE;
if (config.hasProp("nodeSize"))
{
nodeSize = config.getPropInt("nodeSize");
Copy link
Member

Choose a reason for hiding this comment

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

I think this is an example of some code that is made more complicated by having the helper.


std::string compression = config.queryProp("compressed", "default");
toLower(compression);
trim(compression);
Copy link
Member

Choose a reason for hiding this comment

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

I agree, I'm not sure why you would trim this field.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants