From a4d0fba234122d53f864e8f860100088d54e0fe5 Mon Sep 17 00:00:00 2001 From: Jerry Blakley Date: Mon, 29 Jan 2018 20:28:40 +0000 Subject: [PATCH] Stage to master: stat cache negative entries (#428) * Store negative (non-existing) entries in stat cache (#409) --- src/bloom-filter.c | 12 +- src/fusedav.c | 538 ++++++++++++------ src/props.c | 6 +- src/session.c | 4 +- src/statcache.c | 232 ++++++-- src/statcache.h | 12 +- tests/Makefile | 90 ++- tests/README | 159 ++++++ tests/basic-ops.sh | 311 ++++++++++ tests/cltest.sh | 1 + tests/cltest2.sh | 4 +- tests/dual-binding-create.sh | 187 ++++++ tests/dual-binding-test.sh | 149 +++++ tests/dual-binding-tests.sh | 169 ++++++ tests/dual-binding-write.sh | 153 +++++ tests/forensic-haven-cleanup.sh | 2 +- tests/pfbackoff.sh | 249 ++++++++ tests/rename.c | 2 +- tests/urlencode.sh | 4 +- tests/write-delete-checknegative.sh | 215 +++++++ tests/{writefiles.sh => writefiles-create.sh} | 20 +- tests/writefiles-remove.sh | 120 ++++ 22 files changed, 2412 insertions(+), 227 deletions(-) create mode 100644 tests/README create mode 100755 tests/basic-ops.sh create mode 100755 tests/dual-binding-create.sh create mode 100755 tests/dual-binding-test.sh create mode 100755 tests/dual-binding-tests.sh create mode 100755 tests/dual-binding-write.sh create mode 100755 tests/pfbackoff.sh create mode 100755 tests/write-delete-checknegative.sh rename tests/{writefiles.sh => writefiles-create.sh} (83%) create mode 100755 tests/writefiles-remove.sh diff --git a/src/bloom-filter.c b/src/bloom-filter.c index 04082c7e..6d3b9377 100644 --- a/src/bloom-filter.c +++ b/src/bloom-filter.c @@ -193,11 +193,13 @@ static values_t byte_bit_location(unsigned long startvalue, int bits_in_chunk) { int bloomfilter_add(bloomfilter_options_t *options, const void *key, size_t klen) { values_t values; - log_print(LOG_DEBUG, SECTION_BLOOM_DEFAULT, "bloomfilter_add: enter \'%s\' :: salt %ul", key, options->salt); + log_print(LOG_DEBUG, SECTION_BLOOM_DEFAULT, "bloomfilter_add: enter \'%s\' :: salt %lu", key, options->salt); values.hashvalue = options->hashfcn(options->salt, key, klen); for (unsigned int idx = 0; idx < options->num_chunks; idx++) { values = byte_bit_location(values.hashvalue, options->bits_in_chunk); - log_print(LOG_DEBUG, SECTION_BLOOM_DEFAULT, "bloomfilter_add: iter %d :: key: \'%s\' :: salt: %ul :: byte %d :: bit %d", idx, key, values.hashvalue, values.bytevalue, values.bitvalue); + log_print(LOG_DEBUG, SECTION_BLOOM_DEFAULT, + "bloomfilter_add: iter %d :: key: \'%s\' :: salt: %lu :: byte %d :: bit %d", + idx, key, values.hashvalue, values.bytevalue, values.bitvalue); options->bitfield[values.bytevalue] |= values.bitvalue; } return 0; @@ -207,11 +209,13 @@ int bloomfilter_add(bloomfilter_options_t *options, const void *key, size_t klen bool bloomfilter_exists(bloomfilter_options_t * options, const void *key, size_t klen) { values_t values; - log_print(LOG_DEBUG, SECTION_BLOOM_DEFAULT, "bloomfilter_exists: enter \'%s\' :: salt %ul", key, options->salt); + log_print(LOG_DEBUG, SECTION_BLOOM_DEFAULT, "bloomfilter_exists: enter \'%s\' :: salt %lu", key, options->salt); values.hashvalue = options->hashfcn(options->salt, key, klen); for (unsigned int idx = 0; idx < options->num_chunks; idx++) { values = byte_bit_location(values.hashvalue, options->bits_in_chunk); - log_print(LOG_DEBUG, SECTION_BLOOM_DEFAULT, "bloomfilter_exists: iter %d :: key: \'%s\' :: salt: %ul :: byte %d :: bit %d", idx, key, values.hashvalue, values.bytevalue, values.bitvalue); + log_print(LOG_DEBUG, SECTION_BLOOM_DEFAULT, + "bloomfilter_exists: iter %d :: key: \'%s\' :: salt: %lu :: byte %d :: bit %d", + idx, key, values.hashvalue, values.bytevalue, values.bitvalue); if ((options->bitfield[values.bytevalue] & values.bitvalue) == 0) return false; } return true; diff --git a/src/fusedav.c b/src/fusedav.c index 6a03e436..f168f1d1 100644 --- a/src/fusedav.c +++ b/src/fusedav.c @@ -182,82 +182,131 @@ static void getdir_propfind_callback(__unused void *userdata, const char *path, static const char *funcname = "getdir_propfind_callback"; struct fusedav_config *config = fuse_get_context()->private_data; + struct stat_cache_value *existing = NULL; struct stat_cache_value value; GError *subgerr1 = NULL ; - GError *subgerr2 = NULL ; - // Zero-out structure; some fields we don't populate but want to be 0, e.g. st_atim.tv_nsec + log_print(LOG_INFO, SECTION_FUSEDAV_PROP, "%s: %s (%lu)", funcname, path, status_code); + memset(&value, 0, sizeof(struct stat_cache_value)); value.st = st; + // Indicate that this update is the result of a propfind + stat_cache_from_propfind(&value, true); + + // 1. See if we have an item in the local cache + // It might be a positive (existing) entry, or a negative (non-existent) one + existing = stat_cache_value_get(config->cache, path, true, &subgerr1); + if (subgerr1) { + g_propagate_prefixed_error(gerr, subgerr1, "%s: ", funcname); + return; + } - log_print(LOG_INFO, SECTION_FUSEDAV_PROP, "%s: %s (%lu)", - funcname, path, status_code); + /* Notes on dual-binding race conditions and odd 404s + * Binding A does a propfind. + * Binding B does a propfind on a path with multiple directories. + * Say, /abcdir. Binding B starts the propfind at /, and abcdir + * still exists. Then Binding A deletes the directory abcdir. + * Then Binding B's propfind moves to abcdir where it does the + * next stage in its propfind. But abcdir no longer exists. + * The fileserver returns 404, which gets processed in + * props.c:simple_propfind() and turned into a 410 (why? see below), + * and this function gets called, with ctime set to 0. In this case, + * we need to delete the directory entry. + * + * Note that this is completely different from the normal case: + * Binding A removes the directory /abcdir. + * When that is done, Binding B does a propfind. When the propfind + * processes /, the fileserver will note that the directory /abcdir + * has been deleted, and Binding B will call this function via a + * different path in props.c, with an existing st.st_ctime, processed + * in one of the else clauses below. + * + * (When the fileserver detects that a file or directory has + * been deleted outside of a race condition like this, it uses code + * 410 to indicate this. If the fileserver goes to do a propfind + * on a directory which no longer exists as in this race condition, + * it returns a 404. For consistency's sake, simple_propfind() turns + * that 404 into a 410 to be detected here.) + */ - if (status_code == 410) { - struct stat_cache_value *existing; + // 2. If ctime is 0, fileserver has just deleted this item, so no need + // to query again, just delete it - log_print(LOG_INFO, SECTION_FUSEDAV_PROP, "%s: DELETE %s (%lu)", - funcname, path, status_code); - existing = stat_cache_value_get(config->cache, path, true, &subgerr1); - if (subgerr1) { - g_propagate_prefixed_error(gerr, subgerr1, "%s: ", funcname); - return; + // If it exists locally already as a negative item, it will get + // reinserted as a negative item + if (st.st_ctime == 0) { + if (status_code != 410) { + // I don't see how we can get ctime == 0 and status code other than 410 + log_print(LOG_ERR, SECTION_FUSEDAV_PROP, + "%s: unexpected status code on propfind with st_ctime == 0: %lu on %s", + funcname, status_code, path); + g_set_error(gerr, fusedav_quark(), EINVAL, + "%s: Unexpected ctime as 0 but status_code not 410 (%lu)", funcname, status_code); } - - /* Binding A does a propfind. - * Binding B does a propfind on a path with multiple directories. - * Say, /abcdir. Binding B starts the propfind at /, and abcdir - * still exists. Then Binding A deletes the directory abcdir. - * Then Binding B's propfind moves to abcdir where it does the - * next stage in its propfind. But abcdir no longer exists. - * The fileserver returns 404, which gets processed in - * props.c:simple_propfind() and turned into a 410 (why? see below), - * and this function gets called, with ctime set to 0. In this case, - * we need to delete the directory entry. - * - * Note that this is completely different from the normal case: - * Binding A removes the directory /abcdir. - * When that is done, Binding B does a propfind. When the propfind - * processes /, the fileserver will note that the directory /abcdir - * has been deleted, and Binding B will call this function via a - * different path in props.c, with an existing st.st_ctime, processed - * in one of the else clauses below. - * - * (When the fileserver detects that a file or directory has - * been deleted outside of a race condition like this, it uses code - * 410 to indicate this. If the fileserver goes to do a propfind - * on a directory which no longer exists as in this race condition, - * it returns a 404. For consistency's sake, simple_propfind() turns - * that 404 into a 410 to be detected here.) - */ - if (st.st_ctime == 0) { - log_print(LOG_NOTICE, SECTION_FUSEDAV_PROP, + else { + log_print(LOG_WARNING, SECTION_FUSEDAV_PROP, "%s: Path removed on different binding while in the middle of this propfind: %s", funcname, path); - // Fall through to delete + // We'll correct this error if we see it, but check to see if it happens often + if (!stat_cache_is_negative_entry(value)) { + log_print(LOG_WARNING, SECTION_FUSEDAV_PROP, + "%s: Expected negative entry (2); got: %s : st_mode=%d", funcname, path, st.st_mode); + } + // Inserting negative entry + stat_cache_negative_set(&value); + stat_cache_value_set(config->cache, path, &value, &subgerr1); + if (subgerr1) { + g_propagate_prefixed_error(gerr, subgerr1, "%s: ", funcname); + } } - /* If existing indicates that the file existed at a later timestamp than this delete, keep it. */ - /* By returning early, here and other places below, we are not updating the updated field on the file, - * which stat_cache_value_set below would do; we leave that value as it was. We are not seeing a - * creation for this file in the propfind, so it would not be correct to update the updated value as if we had. - */ - else if (existing && existing->updated > st.st_ctime) { + } + + // 3. local update is more recent than propfind returns + + /* Indicates that the local item existed at a later timestamp than what the propfind is indicating, + * so keep whatever is currently in the cache. + * This should be correct both for positive and negative entries in the local cache + */ + else if (existing && existing->updated > st.st_ctime) { + if (status_code == 410) { log_print(LOG_NOTICE, SECTION_FUSEDAV_PROP, "%s: Ignoring outdated removal of path: %s (%lu %lu)", funcname, path, existing->updated, st.st_ctime); - free(existing); - return; } - /* If existing indicates that the file existed at the same timestamp as this delete, - * we don't know which to honor. So query the server to see if it exists there and - * use that. - */ - else if (existing && existing->updated == st.st_ctime) { + else { + log_print(LOG_INFO, SECTION_FUSEDAV_PROP, + "%s: Ignoring outdated creation of path: %s (%lu %lu)", + funcname, path, existing->updated, st.st_ctime); + stat_cache_value_set(config->cache, path, &value, &subgerr1); + } + } + + // 4. Confusion! Two things happen at same time. Query fileserver for canonical view + + /* If existing indicates that the local file existed at the same timestamp as this deletion/creation, + * we don't know which to honor. So query the server to see if it exists there and + * use that. + */ + else if (existing && existing->updated == st.st_ctime) { + bool local_negative = stat_cache_is_negative_entry(*existing); + bool remote_negative = (status_code == 410); + if (local_negative == remote_negative) { + // Nothing to do + log_print(LOG_INFO, SECTION_FUSEDAV_PROP, + "%s: Updated equals ctime, but operations match, nothing to do : %s", funcname, path); + // But we still need to call 'set' to update the local_generation + if (!local_negative) { + stat_cache_value_set(config->cache, path, &value, &subgerr1); + } + } + else { CURL *session = NULL; long response_code = 500; // seed it as bad so we can enter the loop CURLcode res = CURLE_OK; - free(existing); + log_print(LOG_NOTICE, SECTION_FUSEDAV_PROP, + "%s: Updated equals ctime, making HEAD request: %s (%lu %lu)", + funcname, path, existing->updated, st.st_ctime); for (int idx = 0; idx < num_filesystem_server_nodes && (res != CURLE_OK || response_code >= 500); idx++) { bool tmp_session = true; @@ -274,7 +323,7 @@ static void getdir_propfind_callback(__unused void *userdata, const char *path, curl_easy_setopt(session, CURLOPT_NOBODY, 1); log_print(LOG_NOTICE, SECTION_FUSEDAV_PROP, - "%s: saw 410; calling HEAD on %s", funcname, path); + "%s: saw %lu; calling HEAD on %s", funcname, status_code, path); timed_curl_easy_perform(session, &res, &response_code, &elapsed_time); bool non_retriable_error = process_status(funcname, session, res, response_code, elapsed_time, idx, path, tmp_session); @@ -290,59 +339,89 @@ static void getdir_propfind_callback(__unused void *userdata, const char *path, set_dynamic_logging(); g_set_error(gerr, fusedav_quark(), ENETDOWN, "%s: curl failed: %s : rc: %ld\n", funcname, curl_easy_strerror(res), response_code); + // Stick with what we got, either a positive or negative entry + free(existing); return; } else { trigger_saint_event(CLUSTER_SUCCESS); } + // fileserver doesn't think the file exists, so delete it locally + // It's possible what we had locally was a negative entry, but + // making a new negative entry is not harmful. if (response_code >= 400 && response_code < 500) { - // fall through to delete log_print(LOG_NOTICE, SECTION_FUSEDAV_PROP, - "%s: saw 410; executed HEAD; file doesn't exist: %s", - funcname, path); + "%s: saw %lu; executed HEAD; file doesn't exist: %s", + funcname, status_code, path); + // Inserting negative entry + if (st.st_mode != 0) { + log_print(LOG_WARNING, SECTION_FUSEDAV_PROP, + "%s: Expected negative entry (4); got: %s : st_mode=%d", funcname, path, st.st_mode); + } + stat_cache_negative_set(&value); + stat_cache_value_set(config->cache, path, &value, &subgerr1); + if (subgerr1) { + g_propagate_prefixed_error(gerr, subgerr1, "%s: ", funcname); + } } + // fileserver thinks file exists. If we got a 410, we could just keep things the way they were. + // But we'd have to check that what was local wasn't a negative entry. + // Seems wise to just create in all cases else { - // REVIEW: if the file exists, do we want to call stat_cache_value_set and update its ->updated value? if (response_code >= 200 && response_code < 300) { - // We're not deleting since it exists; jump out! log_print(LOG_NOTICE, SECTION_FUSEDAV_PROP, - "%s: saw 410; executed HEAD; file exists: %s", - funcname, path); + "%s: saw %lu; executed HEAD; file exists: %s", + funcname, status_code, path); + stat_cache_value_set(config->cache, path, &value, &subgerr1); + if (subgerr1) { + g_propagate_prefixed_error(gerr, subgerr1, "%s: ", funcname); + } } else { - // On error, prefer retaining a file which should be deleted over deleting a file which should be retained + // On error, retain local file g_set_error(gerr, fusedav_quark(), EINVAL, - "%s(%s): saw 410; HEAD returns unexpected response from curl %ld", - funcname, path, response_code); + "%s(%s): saw %lu; HEAD returns unexpected response from curl %ld", + funcname, path, status_code, response_code); } - return; } } - - log_print(LOG_INFO, SECTION_FUSEDAV_PROP, "%s: Removing path: %s", - funcname, path); - stat_cache_delete(config->cache, path, &subgerr1); - filecache_delete(config->cache, path, true, &subgerr2); - // If we need to combine 2 errors, use one of the error messages in the propagated prefix - if (subgerr1 && subgerr2) { - g_propagate_prefixed_error(gerr, subgerr1, "%s: %s :: ", funcname, subgerr2->message); - } - else if (subgerr1) { - g_propagate_prefixed_error(gerr, subgerr1, "%s: ", funcname); - } - else if (subgerr2) { - g_propagate_prefixed_error(gerr, subgerr2, "%s: ", funcname); - } } + // 5. local entry is older than what propfind is showing + + /* if local entry exists but ctime is older than what propfind indicates, use + * what propfind gives us. On 410, delete, even if what exists is already + * a negative entry; otherwise, create, even if what already exists is a + * positive entry. + * + * Since we're unconditionally creating or deleting regardless of what is + * existing, we can use this code whether or not there is an existing item + */ else { - log_print(LOG_INFO, SECTION_FUSEDAV_PROP, "%s: CREATE %s (%lu)", - funcname, path, status_code); - stat_cache_value_set(config->cache, path, &value, &subgerr1); - if (subgerr1) { - g_propagate_prefixed_error(gerr, subgerr1, "%s: ", funcname); - return; + log_print(LOG_DEBUG, SECTION_FUSEDAV_PROP, "%s: status_code: %lu; normal case, deleting or creating: %s", + funcname, status_code, path); + if (status_code == 410) { + // Inserting negative entry + if (st.st_mode != 0) { + log_print(LOG_WARNING, SECTION_FUSEDAV_PROP, + "%s: Expected negative entry (5); got: %s : st_mode=%d", funcname, path, st.st_mode); + } + log_print(LOG_NOTICE, SECTION_FUSEDAV_PROP, "%s: normal case, deleting: %s", + funcname, path); + stat_cache_negative_set(&value); + stat_cache_value_set(config->cache, path, &value, &subgerr1); + if (subgerr1) { + g_propagate_prefixed_error(gerr, subgerr1, "%s: ", funcname); + } + } + else { + stat_cache_value_set(config->cache, path, &value, &subgerr1); + if (subgerr1) { + g_propagate_prefixed_error(gerr, subgerr1, "%s: ", funcname); + } } } + + if (existing) free(existing); } static void getdir_cache_callback(__unused const char *path_prefix, const char *filename, void *user) { @@ -536,8 +615,7 @@ static void getattr_propfind_callback(__unused void *userdata, const char *path, unsigned long status_code, GError **gerr) { struct fusedav_config *config = fuse_get_context()->private_data; struct stat_cache_value value; - GError *subgerr1 = NULL; - GError *subgerr2 = NULL; + GError *subgerr = NULL; // Zero-out structure; some fields we don't populate but want to be 0, e.g. st_atim.tv_nsec memset(&value, 0, sizeof(struct stat_cache_value)); @@ -545,32 +623,26 @@ static void getattr_propfind_callback(__unused void *userdata, const char *path, if (status_code == 410) { log_print(LOG_NOTICE, SECTION_FUSEDAV_PROP, "getattr_propfind_callback: Deleting from stat cache: %s", path); - stat_cache_delete(config->cache, path, &subgerr1); - filecache_delete(config->cache, path, true, &subgerr2); - - // If we need to combine 2 errors, use one of the error messages in the propagated prefix - if (subgerr1 && subgerr2) { - log_print(LOG_NOTICE, SECTION_FUSEDAV_PROP, "getattr_propfind_callback: %s: %s: %s", path, subgerr1->message, subgerr2->message); - g_propagate_prefixed_error(gerr, subgerr1, "getattr_propfind_callback: %s :: ", subgerr2->message); - return; - } - else if (subgerr1) { - log_print(LOG_NOTICE, SECTION_FUSEDAV_PROP, "getattr_propfind_callback: %s: %s", path, subgerr1->message); - g_propagate_prefixed_error(gerr, subgerr1, "getattr_propfind_callback: "); - return; + // Create a negative entry + if (st.st_mode != 0) { + log_print(LOG_WARNING, SECTION_FUSEDAV_PROP, + "getattr_propfind_callback: Expected negative entry; got: %s : st_mode=%d", path, st.st_mode); } - else if (subgerr2) { - log_print(LOG_NOTICE, SECTION_FUSEDAV_PROP, "getattr_propfind_callback: %s: %s", path, subgerr2->message); - g_propagate_prefixed_error(gerr, subgerr2, "getattr_propfind_callback: "); + value.st.st_mode = 0; // Make it so, even if it already is + stat_cache_value_set(config->cache, path, &value, &subgerr); + + if (subgerr) { + log_print(LOG_NOTICE, SECTION_FUSEDAV_PROP, "getattr_propfind_callback: %s: %s", path, subgerr->message); + g_propagate_prefixed_error(gerr, subgerr, "getattr_propfind_callback: "); return; } } else { log_print(LOG_INFO, SECTION_FUSEDAV_PROP, "getattr_propfind_callback: Adding to stat cache: %s", path); - stat_cache_value_set(config->cache, path, &value, &subgerr1); - if (subgerr1) { - log_print(LOG_NOTICE, SECTION_FUSEDAV_PROP, "getattr_propfind_callback: %s: %s", path, subgerr1->message); - g_propagate_prefixed_error(gerr, subgerr1, "getattr_propfind_callback: "); + stat_cache_value_set(config->cache, path, &value, &subgerr); + if (subgerr) { + log_print(LOG_NOTICE, SECTION_FUSEDAV_PROP, "getattr_propfind_callback: %s: %s", path, subgerr->message); + g_propagate_prefixed_error(gerr, subgerr, "getattr_propfind_callback: "); return; } } @@ -612,36 +684,122 @@ static int get_stat_from_cache(const char *path, struct stat *stbuf, enum ignore // else we're not skipping the freshness check and maybe the item is in the cache but just out of date log_print(LOG_DYNAMIC, SECTION_FUSEDAV_STAT, "get_stat_from_cache: Treating key as absent or expired for path %s.", path); return -EKEYEXPIRED; + } + // If st_mode is 0, this is a negative (non-existing) entry + else if (response->st.st_mode == 0) { + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "get_stat_from_cache: negative entry response from stat_cache_value_get for path %s.", path); + return -ENOENT; } log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "get_stat_from_cache: Got response from stat_cache_value_get for path %s.", path); *stbuf = response->st; - print_stat(stbuf, "stat_cache_value_get response"); + print_stat(stbuf, "stat_cache_value_get response", path); free(response); - log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "get_stat_from_cache(%s, stbuf, %d): returns %s", path, skip_freshness_check, stbuf->st_mode ? "0" : "ENOENT"); - if (stbuf->st_mode == 0 || inject_error(fusedav_error_statstmode)) { - g_set_error(gerr, fusedav_quark(), ENOENT, "get_stat_from_cache: stbuf mode is 0: "); - return -1; - } + log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "get_stat_from_cache(%s, stbuf, %d)", path, skip_freshness_check); + return 0; +} +// If its a nonnegative entry and outside the TTL, even if 0, then needs_propfind is true +// If its a negative entry, then the TTL is a fibonacci sequence +static bool requires_propfind(const char *path, time_t time_since, GError **gerr) { + struct fusedav_config *config = fuse_get_context()->private_data; + const char *funcname = "requires_propfind"; + struct stat_cache_value *value = NULL; + bool skip_freshness_check = true; + GError *subgerr = NULL; + + // Get the entry + value = stat_cache_value_get(config->cache, path, skip_freshness_check, &subgerr); + + // Check for error and return + if (subgerr) { + g_propagate_prefixed_error(gerr, subgerr, "%s: ", funcname); + return true; + } + + // If a value was returned from the stat cache ... + if (value != NULL) { + // And the path exists in the cache ... + if (!stat_cache_is_negative_entry(*value)) { + // Requested path exists as non-negative entry. + // If stale, (time_since > STAT_CACHE_NEGATIVE_TTL) return true + // Otherwise, return false + // If TTL is 0, this still works. + // NB Given our current configuration, I don't think we can see + // a non-stale real (nonnegative) value. All non-stale real + // values will have been returned as fresh before this gets called. + bool stale = time_since > STAT_CACHE_NEGATIVE_TTL; + if (stale) { + stats_counter_local("propfind-nonnegative-cache", 1, pfsamplerate); + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "%s: true on non-negative entry %s", funcname, path); + } else { + stats_counter_local("propfind-negative-cache", 1, pfsamplerate); + BUMP(propfind_negative_cache); + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "%s: false on non-negative %s", funcname, path); + } + return stale; + } + // The value was tagged as a negative (non-existent) value ... + else { + time_t current_time = time(NULL); + time_t next_time; + + next_time = stat_cache_next_propfind(*value); + + // Our "time to next propfind" is still in the future, so no propfind ... + if (next_time > current_time) { + stats_counter_local("propfind-negative-cache", 1, pfsamplerate); + BUMP(propfind_negative_cache); + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, + "%s: no propfind needed yet on negative entry %s", funcname, path); + log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, + "%s: no propfind needed yet on negative entry %s; now:next:upd:made--%lu:%lu:%lu", + funcname, path, current_time, next_time, value->updated); + return false; + } else { + // Time for a new propfind + stats_counter_local("propfind-negative-ttl-expired", 1, pfsamplerate); + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, + "%s; new propfind for path: %s, c/nt: %lu: %lu", + funcname, path, current_time, next_time); + + return true; + } + } + } else { + // No value. We haven't previously cached its non-existence, so assume + // this is the first attempt at this item + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "%s; first propfind for path: %s", + funcname, path); + + stats_counter_local("propfind-nonnegative-cache", 1, pfsamplerate); + return true; + } } static void get_stat(const char *path, struct stat *stbuf, GError **gerr) { struct fusedav_config *config = fuse_get_context()->private_data; + const char *funcname = "get_stat"; char *parent_path = NULL; + struct stat_cache_value value; GError *tmpgerr = NULL; + GError *subgerr = NULL; time_t parent_children_update_ts; bool is_base_directory; + bool needs_propfind; int ret = -ENOENT; enum ignore_freshness skip_freshness_check = OFF; time_t time_since; memset(stbuf, 0, sizeof(struct stat)); + memset(&value, 0, sizeof(struct stat_cache_value)); + // Reset from_propfind to false before starting, to clear any previous value + stat_cache_from_propfind(&value, false); - log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "get_stat(%s, stbuf)", path); + log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "%s:(%s, stbuf)", funcname, path); - log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "Checking if path %s matches base directory.", path); + log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "%s: Checking if path %s matches base directory.", funcname, path); is_base_directory = (strcmp(path, "/") == 0); // If it's the root directory and all attributes are specified, construct a response. @@ -650,16 +808,16 @@ static void get_stat(const char *path, struct stat *stbuf, GError **gerr) { // mode = 0 (unspecified), is_dir = true; fd = -1, irrelevant for dir fill_stat_generic(stbuf, 0, true, -1, &tmpgerr); if (tmpgerr) { - g_propagate_prefixed_error(gerr, tmpgerr, "get_stat: "); + g_propagate_prefixed_error(gerr, tmpgerr, "%s: ", funcname); return; } - log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "Used constructed stat data for base directory."); + log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "%s: Used constructed stat data for base directory.", funcname); return; } if (config->grace && use_saint_mode()) { - log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "get_stat: Using saint mode on %s", path); + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "%s: Using saint mode on %s", funcname, path); skip_freshness_check = SAINT_MODE; } @@ -669,7 +827,7 @@ static void get_stat(const char *path, struct stat *stbuf, GError **gerr) { // Propagate the error but let the rest of the logic determine return value // Unless we change the logic in get_stat_from_cache, it will return ENOENT or ENETDOWN if (tmpgerr) { - g_propagate_prefixed_error(gerr, tmpgerr, "get_stat: "); + g_propagate_prefixed_error(gerr, tmpgerr, "%s: ", funcname); return; } else if (ret == 0) { @@ -677,38 +835,37 @@ static void get_stat(const char *path, struct stat *stbuf, GError **gerr) { } // else fall through, this would be for EKEYEXPIRED, indicating statcache miss - log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "STAT-CACHE-MISS"); + log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "%s: STAT-CACHE-MISS", funcname); // If it's the root directory or refresh_dir_for_file_stat is false, // just do a single, zero-depth PROPFIND. if (!config->refresh_dir_for_file_stat || is_base_directory) { - GError *subgerr = NULL; - log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "Performing zero-depth PROPFIND on path: %s", path); + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "%s: Performing zero-depth PROPFIND on path: %s", funcname, path); stats_counter_local("propfind-root", 1, pfsamplerate); ret = simple_propfind_with_redirect(path, PROPFIND_DEPTH_ZERO, 0, getattr_propfind_callback, NULL, &subgerr); if (subgerr) { // Delete from cache on error; ignore errors from stat_cache_delete since we already have one - g_propagate_prefixed_error(gerr, subgerr, "get_stat: "); - stat_cache_delete(config->cache, path, NULL); + g_propagate_prefixed_error(gerr, subgerr, "%s: ", funcname); + stat_cache_value_set(config->cache, path, &value, NULL); goto fail; } else if (ret < 0) { // We don't ever expect ret < 0 without gerr - stat_cache_delete(config->cache, path, &subgerr); + stat_cache_value_set(config->cache, path, &value, &subgerr); if (subgerr) { - g_propagate_prefixed_error(gerr, subgerr, "get_stat: PROPFIND failed"); + g_propagate_prefixed_error(gerr, subgerr, "%s: PROPFIND failed", funcname); goto fail; } - g_set_error(gerr, fusedav_quark(), ENETDOWN, "get_stat: PROPFIND failed"); + g_set_error(gerr, fusedav_quark(), ENETDOWN, "%s: PROPFIND failed", funcname); goto fail; } - log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "Zero-depth PROPFIND succeeded: %s", path); + log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "%s: Zero-depth PROPFIND succeeded: %s", funcname, path); // The propfind succeeded, so if the item does not exist, then it is ENOENT. skip_freshness_check = ALREADY_FRESH; // If in saint mode and the item wasn't already in the stat cache, this will return ENETDOWN. get_stat_from_cache(path, stbuf, skip_freshness_check, &subgerr); if (subgerr) { - g_propagate_prefixed_error(gerr, subgerr, "get_stat: "); + g_propagate_prefixed_error(gerr, subgerr, "%s: ", funcname); goto fail; } // success (we could just return, but it looks more consistent to goto finish after the goto fails above @@ -721,13 +878,14 @@ static void get_stat(const char *path, struct stat *stbuf, GError **gerr) { parent_path = path_parent(path); if (parent_path == NULL) goto fail; - log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "Getting parent path entry: %s", parent_path); + log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "%s: Getting parent path entry: %s", funcname, parent_path); parent_children_update_ts = stat_cache_read_updated_children(config->cache, parent_path, &tmpgerr); if (tmpgerr) { - g_propagate_prefixed_error(gerr, tmpgerr, "get_stat: "); + g_propagate_prefixed_error(gerr, tmpgerr, "%s: ", funcname); goto fail; } - log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "Parent was updated: %s %lu", parent_path, parent_children_update_ts); + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, + "%s: Parent was updated: %s %lu", funcname, parent_path, parent_children_update_ts); /* When new files are being uploaded to fresh directories, it will trigger complete propfinds * which can be expensive. We saw also where we saw repeated complete propfinds to the same @@ -761,31 +919,77 @@ static void get_stat(const char *path, struct stat *stbuf, GError **gerr) { time_since = time(NULL) - parent_children_update_ts; // Keep stats for each second 0-6, then bucket everything over 6 stats_histo("profind_ttl", time_since, 6, pfsamplerate); - // If the parent directory is out of date, update it. - if (time_since > STAT_CACHE_NEGATIVE_TTL) { - GError *subgerr = NULL; + needs_propfind = requires_propfind(path, time_since, &tmpgerr); + if (tmpgerr) { + g_propagate_prefixed_error(gerr, tmpgerr, "%s: ", funcname); + goto fail; + } - stats_counter_local("propfind-nonnegative-cache", 1, pfsamplerate); - log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "get_stat: Calling update_directory: %s; attempt_progressive_update will be %d", - parent_path, (parent_children_update_ts > 0)); + if (needs_propfind) { + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "%s: Calling update_directory: %s; attempt_progressive_update will be %d", + funcname, parent_path, (parent_children_update_ts > 0)); // If parent_children_update_ts is 0, there are no entries for updated_children in statcache // In that case, skip the progressive propfind and go straight to complete propfind update_directory(parent_path, (parent_children_update_ts > 0), &subgerr); if (subgerr) { - g_propagate_prefixed_error(gerr, subgerr, "get_stat: "); + g_propagate_prefixed_error(gerr, subgerr, "%s: ", funcname); goto fail; } - } else { - stats_counter_local("propfind-negative-cache", 1, pfsamplerate); - BUMP(propfind_negative_cache); - } + } // Try again to hit the file in the stat cache. skip_freshness_check = ALREADY_FRESH; ret = get_stat_from_cache(path, stbuf, skip_freshness_check, &tmpgerr); if (tmpgerr) { - log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "get_stat: propagating error from get_stat_from_cache on %s", path); - g_propagate_prefixed_error(gerr, tmpgerr, "get_stat: "); + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, + "%s: error from get_stat_from_cache on %s :: %d", funcname, path, tmpgerr->code); + if (tmpgerr->code == ENOENT) { + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "%s: ENOENT on %s; creating negative entry", funcname, path); + // Only need to adjust negative entry if there really was a propfind + if (needs_propfind) { + log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, + "%s: creating negative entry (A). %s: st_mode: %d", funcname, path, stbuf->st_mode); + // Add a negative entry + if (stbuf->st_mode != 0) { + log_print(LOG_WARNING, SECTION_FUSEDAV_PROP, + "%s: Expected negative entry (a); got: %s : st_mode=%d", funcname, path, stbuf->st_mode); + } + stat_cache_from_propfind(&value, true); + value.st = *stbuf; + value.st.st_mode = 0; // Make it so, even if it already is + log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "%s: ENOENT on %s; calling stat_cache_value_set on if", funcname, path); + stat_cache_value_set(config->cache, path, &value, &subgerr); + } + if (subgerr) { + g_propagate_prefixed_error(gerr, subgerr, "%s: ENOENT: ", funcname); + goto fail; + } + } + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "%s: propagating error from get_stat_from_cache on %s", funcname, path); + g_propagate_prefixed_error(gerr, tmpgerr, "%s: ", funcname); + goto fail; + } else if (ret == -ENOENT) { + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "%s: ENOENT from get_stat_from_cache on %s", funcname, path); + // Only need to adjust negative entry if there really was a propfind + if (needs_propfind) { + log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, + "%s: creating negative entry (B). %s: st_mode: %d", funcname, path, stbuf->st_mode); + // Add a negative entry + if (stbuf->st_mode != 0) { + log_print(LOG_WARNING, SECTION_FUSEDAV_PROP, + "%s: Expected negative entry (b); got: %s : st_mode=%d", funcname, path, stbuf->st_mode); + } + stat_cache_from_propfind(&value, true); + value.st = *stbuf; + value.st.st_mode = 0; // Make it so, even if it already is + log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "%s: ENOENT on %s; calling stat_cache_value_set on else", funcname, path); + stat_cache_value_set(config->cache, path, &value, &subgerr); + } + if (subgerr) { + g_propagate_prefixed_error(gerr, subgerr, "%s: ENOENT: ", funcname); + goto fail; + } + g_set_error(gerr, fusedav_quark(), ENOENT, "%s: ENOENT", funcname); goto fail; } if (ret == 0) goto finish; @@ -891,7 +1095,7 @@ static int dav_getattr(const char *path, struct stat *stbuf) { } return processed_gerror("dav_getattr: ", path, &gerr); } - print_stat(stbuf, "dav_getattr"); + print_stat(stbuf, "dav_getattr", path); log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "Done: dav_getattr(%s)", path); return 0; @@ -901,6 +1105,7 @@ static void common_unlink(const char *path, bool do_unlink, GError **gerr) { static const char *funcname = "common_unlink"; struct fusedav_config *config = fuse_get_context()->private_data; struct stat st; + struct stat_cache_value value; GError *gerr2 = NULL; GError *gerr3 = NULL; @@ -960,8 +1165,10 @@ static void common_unlink(const char *path, bool do_unlink, GError **gerr) { log_print(LOG_DEBUG, SECTION_FUSEDAV_FILE, "%s: calling filecache_delete on %s", funcname, path); filecache_delete(config->cache, path, true, &gerr2); - log_print(LOG_DEBUG, SECTION_FUSEDAV_FILE, "%s: calling stat_cache_delete on %s", funcname, path); - stat_cache_delete(config->cache, path, &gerr3); + memset(&value, 0, sizeof(struct stat_cache_value)); + log_print(LOG_DEBUG, SECTION_FUSEDAV_FILE, "%s: calling stat_cache_negative_entry on %s", funcname, path); + stat_cache_value_set(config->cache, path, &value, &gerr3); + // stat_cache_negative_entry(config->cache, path, update, &gerr3); // If we need to combine 2 errors, use one of the error messages in the propagated prefix if (gerr2 && gerr3) { @@ -996,7 +1203,7 @@ static int dav_unlink(const char *path) { return processed_gerror("dav_unlink: ", path, &gerr); } - log_print(LOG_INFO, SECTION_FUSEDAV_FILE, "dav_unlink: deleted \"%s\")", path); + log_print(LOG_NOTICE, SECTION_FUSEDAV_FILE, "dav_unlink: deleted \"%s\")", path); return 0; } @@ -1006,6 +1213,7 @@ static int dav_rmdir(const char *path) { GError *gerr = NULL; char fn[PATH_MAX]; bool has_child; + struct stat_cache_value value; struct stat st; long response_code = 500; // seed it as bad so we can enter the loop CURLcode res = CURLE_OK; @@ -1025,15 +1233,16 @@ static int dav_rmdir(const char *path) { return processed_gerror(funcname, path, &gerr); } + update_directory(path, true, &gerr); + if (gerr) { + return processed_gerror(funcname, path, &gerr); + } + if (!S_ISDIR(st.st_mode)) { log_print(LOG_INFO, SECTION_FUSEDAV_DIR, "%s: failed to remove `%s\': Not a directory", funcname, path); return -ENOTDIR; } - // The slash should force it to find entries in the directory after the slash, and - // not the directory itself - snprintf(fn, sizeof(fn), "%s/", path); - // Check to see if it is empty ... // get_stat already called update_directory, which called stat_cache_updated_children // so the stat cache should be up to date. @@ -1043,6 +1252,10 @@ static int dav_rmdir(const char *path) { return -ENOTEMPTY; } + // The slash should force it to find entries in the directory after the slash, and + // not the directory itself + snprintf(fn, sizeof(fn), "%s/", path); + for (int idx = 0; idx < num_filesystem_server_nodes && (res != CURLE_OK || response_code >= 500); idx++) { CURL *session; struct curl_slist *slist = NULL; @@ -1081,7 +1294,9 @@ static int dav_rmdir(const char *path) { log_print(LOG_DEBUG, SECTION_FUSEDAV_DIR, "%s: removed(%s)", funcname, path); - stat_cache_delete(config->cache, path, &gerr); + memset(&value, 0, sizeof(struct stat_cache_value)); + stat_cache_value_set(config->cache, path, &value, &gerr); + // stat_cache_negative_entry(config->cache, path, update, &gerr); if (gerr) { return processed_gerror(funcname, path, &gerr); } @@ -1179,6 +1394,7 @@ static int dav_rename(const char *from, const char *to) { struct stat st; char fn[PATH_MAX]; struct stat_cache_value *entry = NULL; + struct stat_cache_value value; long response_code = 500; // seed it as bad so we can enter the loop CURLcode res = CURLE_OK; @@ -1197,7 +1413,7 @@ static int dav_rename(const char *from, const char *to) { get_stat(from, &st, &gerr); if (gerr) { - server_ret = processed_gerror("dav_rmdir: ", from, &gerr); + server_ret = processed_gerror("dav_rename: ", from, &gerr); goto finish; } @@ -1300,7 +1516,9 @@ static int dav_rename(const char *from, const char *to) { goto finish; } - stat_cache_delete(config->cache, from, &gerr); + memset(&value, 0, sizeof(struct stat_cache_value)); + stat_cache_value_set(config->cache, from, &value, &gerr); + // stat_cache_negative_entry(config->cache, from, update, &gerr); if (gerr) { local_ret = processed_gerror(funcname, from, &gerr); goto finish; diff --git a/src/props.c b/src/props.c index 0560237f..c4fdf852 100644 --- a/src/props.c +++ b/src/props.c @@ -352,7 +352,7 @@ static size_t header_callback(void *contents, size_t length, size_t nmemb, __unu "header_callback: got readonly:'%s'", header); } else if (strcasestr(header, "readwrite") != NULL) { clear_readonly_mode(); - log_print(LOG_INFO, SECTION_PROPS_DEFAULT, + log_print(LOG_DEBUG, SECTION_PROPS_DEFAULT, "header_callback: got readwrite:'%s'", header); } else { // If we get something other than readonly or readwrite, punt. Default is read-write @@ -363,7 +363,7 @@ static size_t header_callback(void *contents, size_t length, size_t nmemb, __unu } } - log_print(LOG_INFO, SECTION_PROPS_DEFAULT, "header_callback: '%s' :: %d, %d", header, length, nmemb); + log_print(LOG_DEBUG, SECTION_PROPS_DEFAULT, "header_callback: '%s' :: %d, %d", header, length, nmemb); free(header); return length * nmemb; } @@ -441,7 +441,7 @@ int simple_propfind(const char *path, size_t depth, time_t last_updated, props_r ""); // Perform the request and parse the response. - log_print(LOG_INFO, SECTION_PROPS_DEFAULT, "%s: About to perform (%s) PROPFIND (%ul).", + log_print(LOG_INFO, SECTION_PROPS_DEFAULT, "%s: About to perform (%s) PROPFIND (%lu).", funcname, last_updated > 0 ? "progressive" : "complete", last_updated); timed_curl_easy_perform(session, &res, &response_code, &elapsed_time); diff --git a/src/session.c b/src/session.c index dc152b4d..34dab8ff 100644 --- a/src/session.c +++ b/src/session.c @@ -432,11 +432,11 @@ static int session_debug(__unused CURL *handle, curl_infotype type, char *data, // We want to see the "Trying message, but the others only when in some // level of debug if (strstr(msg, "Trying")) { - log_print(LOG_INFO, SECTION_SESSION_DEFAULT, "cURL: %s", msg); + log_print(LOG_DEBUG, SECTION_SESSION_DEFAULT, "cURL: %s", msg); print_ipaddr_pair(msg); } else { - log_print(LOG_INFO, SECTION_SESSION_DEFAULT, "cURL: %s", msg); + log_print(LOG_DEBUG, SECTION_SESSION_DEFAULT, "cURL: %s", msg); } free(msg); } diff --git a/src/statcache.c b/src/statcache.c index 6ed39d36..398d4179 100644 --- a/src/statcache.c +++ b/src/statcache.c @@ -74,8 +74,8 @@ unsigned long stat_cache_get_local_generation(void) { return ret; } -int print_stat(struct stat *stbuf, const char *title) { - log_print(LOG_DEBUG, SECTION_STATCACHE_OUTPUT, "stat: %s", title); +int print_stat(struct stat *stbuf, const char *title, const char *path) { + log_print(LOG_DEBUG, SECTION_STATCACHE_OUTPUT, "stat: %s :: %s", title, path); log_print(LOG_DEBUG, SECTION_STATCACHE_OUTPUT, " .st_mode=%04o", stbuf->st_mode); log_print(LOG_DEBUG, SECTION_STATCACHE_OUTPUT, " .st_nlink=%ld", stbuf->st_nlink); log_print(LOG_DEBUG, SECTION_STATCACHE_OUTPUT, " .st_uid=%d", stbuf->st_uid); @@ -290,6 +290,7 @@ void stat_cache_open(stat_cache_t **cache, struct stat_cache_supplemental *suppl log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "%s: Cache does not exist, using new data version %d", funcname); data_version = STAT_CACHE_DATA_VERSION; } + log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "%s: Using data version %d", funcname, data_version); stat_cache_data_version_set(*cache, data_version, &subgerr); @@ -355,6 +356,13 @@ struct stat_cache_value *stat_cache_value_get(stat_cache_t *cache, const char *p stats_counter("statcache_miss", 1, pfsamplerate); return NULL; } + // If this is a negative entry, we need to return the value so the entry can be processed + else if (value->st.st_mode == 0) { + log_print(LOG_INFO, SECTION_STATCACHE_CACHE, "stat_cache_value_get: negative entry on path: %s", path); + stats_counter("statcache_negative_entry", 1, pfsamplerate); + print_stat(&value->st, "stat_cache_value_get", path); + return value; + } if (vallen != sizeof(struct stat_cache_value)) { g_set_error (gerr, leveldb_quark(), E_SC_LDBERR, "stat_cache_value_get: Length %lu is not expected length %lu.", vallen, sizeof(struct stat_cache_value)); @@ -403,7 +411,8 @@ struct stat_cache_value *stat_cache_value_get(stat_cache_t *cache, const char *p directory, time_since); free(directory); if (time_since > CACHE_TIMEOUT) { - log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "stat_cache_value_get: %s is too old (%lu seconds).", + log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, + "stat_cache_value_get: Parent directory for %s is too old (%lu seconds).", path, time_since); free(value); stats_counter("statcache_stale", 1, pfsamplerate); @@ -501,13 +510,127 @@ time_t stat_cache_read_updated_children(stat_cache_t *cache, const char *path, G return ret; } +// Use atime to store the next time we want a propfind +time_t stat_cache_next_propfind(struct stat_cache_value value) { + return value.st.st_atime; +} + +// A negative entry is an item in the cache which represents a miss, +// so we can cache its non-existence and regulate how often +// we make a propfind request to the server to check if it has +// come into existence. +bool stat_cache_is_negative_entry(struct stat_cache_value value) { + // The struct stat st gets zero'ed out when we put a negative entry in the cache. + // If an extant item is put in the cache, at st_mode will be non-zero. + // So use st_mode as our check for non-existence + if (value.st.st_mode == 0) return true; + else return false; +} + +// We use st_mode == 0 to denote that the entry is a negative (non-existing) one +void stat_cache_negative_set(struct stat_cache_value *value) { + value->st.st_mode = 0; +} + +// We need to know if this access was from a propfind in order to +// correctly note when to next call propfind on a negative entry +void stat_cache_from_propfind(struct stat_cache_value *value, bool bvalue) { + value->from_propfind = bvalue; +} + +// Determine the next fibs value (if we've reached it) +// curvalue is always a fib +static int next_fibs(const int curvalue, const int idx) { + // A negative entry will gradually wait longer and longer to retry until + // about 10 minutes (610 seconds); then every 610 seconds thereafter + static const int fibs[] = {0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610}; + static const int fibs_len = sizeof(fibs) / sizeof(int); + + // If we're at the last element already, return that last indexed value + // (It should never be greater than fibs_len - 1; just being careful) + if (idx >= fibs_len - 1) return fibs[fibs_len - 1]; + + // If an exact match, return the next indexed value + if (curvalue == fibs[idx]) return fibs[idx + 1]; + + // The sentinel + return -1; +} + +// Create or update a negative entry in the stat cache for a deleted or non-existent object +static void stat_cache_negative_entry(stat_cache_t *cache, const char *path, struct stat_cache_value *value, GError **gerr) { + static const char *funcname = "stat_cache_negative_entry"; + struct stat_cache_value *existing = NULL; + GError *subgerr = NULL ; + + existing = stat_cache_value_get(cache, path, true, &subgerr); + if (subgerr) { + g_propagate_prefixed_error(gerr, subgerr, "%s: failed on stat_cache_get for %s", funcname, path); + return; + } + + if (existing) { + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "%s: incrementing entry %s", funcname, path); + // Keep mtime constant, equal to the time when the negative entry was created + // atime then is incremented by the next fib + if (stat_cache_is_negative_entry(*existing)) { + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "%s: negative entry %s", funcname, path); + if (value->from_propfind) { + int curfib; + int nextfib; + int idx = 0; + curfib = existing->st.st_atime - existing->st.st_mtime; + nextfib = next_fibs(curfib, idx); + while (nextfib == -1) { + idx++; + nextfib = next_fibs(curfib, idx); + } + + // Reset mtime to the current atime, and atime to the nextfib increment + value->st.st_mtime = existing->st.st_atime; + value->st.st_atime += existing->st.st_atime + nextfib; + log_print(LOG_NOTICE, SECTION_FUSEDAV_STAT, "%s: negative entry from propfind %s", funcname, path); + log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, + "%s: %s: negative entry from propfind; mode: %lu; atime: %lu; mtime: %lu", + funcname, path, value->st.st_mode, value->st.st_atime, value->st.st_mtime); + } + else { + // Just copy the st from existing to value + value->st = existing->st; + log_print(LOG_NOTICE, SECTION_FUSEDAV_STAT, + "%s: %s: negative entry not from propfind; mode: %lu; atime: %lu; mtime: %lu", + funcname, path, value->st.st_mode, value->st.st_atime, value->st.st_mtime); + } + } + free(existing); + } + else { + time_t curtime = time(NULL); + log_print(LOG_INFO, SECTION_FUSEDAV_STAT, "%s: creating entry %s", funcname, path); + value->st.st_mtime = curtime; + value->st.st_atime = curtime + 1; // the next fib + } + + // Check for error and return + if (subgerr) { + g_propagate_prefixed_error(gerr, subgerr, "%s: failed setting new negative entry for %s", funcname, path); + } + + log_print(LOG_DEBUG, SECTION_FUSEDAV_STAT, "%s: %s: mode: %lu; atime: %lu; mtime: %lu", + funcname, path, value->st.st_mode, value->st.st_atime, value->st.st_mtime); + + return; +} + void stat_cache_value_set(stat_cache_t *cache, const char *path, struct stat_cache_value *value, GError **gerr) { + static const char *funcname = "stat_cache_value_set"; leveldb_writeoptions_t *options; + GError *subgerr = NULL ; char *errptr = NULL; char *key; if (path == NULL) { - log_print(LOG_NOTICE, SECTION_STATCACHE_CACHE, "stat_cache_value_set: input path is null"); + log_print(LOG_NOTICE, SECTION_STATCACHE_CACHE, "%s: input path is null", funcname); return; } @@ -515,12 +638,23 @@ void stat_cache_value_set(stat_cache_t *cache, const char *path, struct stat_cac assert(value); + if (stat_cache_is_negative_entry(*value)) { + // Update value with negative entry values + stat_cache_negative_entry(cache, path, value, &subgerr); + if (subgerr) { + g_propagate_prefixed_error(gerr, subgerr, "%s: failed on stat_cache_negative_entry for %s", funcname, path); + return; + } + log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "%s: negative_entry; %s (mode %04o: atime %lu: mtime %lu)", + funcname, path, value->st.st_mode, value->st.st_atime, value->st.st_mtime); + } + value->updated = time(NULL); value->local_generation = stat_cache_get_local_generation(); key = path2key(path, false); - log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "CSET: %s (mode %04o: updated %lu: loc_gen %lu)", - key, value->st.st_mode, value->updated, value->local_generation); + log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "%s: %s (mode %04o: updated %lu: loc_gen %lu: atime %lu: mtime %lu)", + funcname, key, value->st.st_mode, value->updated, value->local_generation, value->st.st_atime, value->st.st_mtime); options = leveldb_writeoptions_create(); leveldb_put(cache, options, key, strlen(key) + 1, (char *) value, sizeof(struct stat_cache_value), &errptr); @@ -529,9 +663,9 @@ void stat_cache_value_set(stat_cache_t *cache, const char *path, struct stat_cac free(key); if (errptr != NULL || inject_error(statcache_error_setldb)) { - g_set_error (gerr, leveldb_quark(), E_SC_LDBERR, "stat_cache_value_set: leveldb_set error: %s", errptr ? errptr : "inject-error"); + g_set_error (gerr, leveldb_quark(), E_SC_LDBERR, "%s: leveldb_set error: %s", funcname, errptr ? errptr : "inject-error"); free(errptr); - log_print(LOG_ALERT, SECTION_STATCACHE_CACHE, "stat_cache_value_set: leveldb_get error, kill fusedav process"); + log_print(LOG_ALERT, SECTION_STATCACHE_CACHE, "%s: leveldb_get error, kill fusedav process", funcname); kill(getpid(), SIGTERM); return; } @@ -569,15 +703,20 @@ void stat_cache_delete(stat_cache_t *cache, const char *path, GError **gerr) { void stat_cache_delete_parent(stat_cache_t *cache, const char *path, GError **gerr) { char *p; GError *tmpgerr = NULL; + struct stat_cache_value value; BUMP(statcache_del_parent); + // Zero-out structure; some fields we don't populate but want to be 0, e.g. st_atim.tv_nsec + memset(&value, 0, sizeof(struct stat_cache_value)); + log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "stat_cache_delete_parent: %s", path); if ((p = path_parent(path))) { log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "stat_cache_delete_parent: deleting parent %s", p); - stat_cache_delete(cache, p, &tmpgerr); + stat_cache_negative_set(&value); + stat_cache_value_set(cache, path, &value, &tmpgerr); if (tmpgerr) { g_propagate_prefixed_error(gerr, tmpgerr, "stat_cache_delete_parent: "); } @@ -591,7 +730,8 @@ void stat_cache_delete_parent(stat_cache_t *cache, const char *path, GError **ge } else { log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "stat_cache_delete_parent: not deleting parent, deleting child %s", path); - stat_cache_delete(cache, path, &tmpgerr); + stat_cache_negative_set(&value); + stat_cache_value_set(cache, path, &value, &tmpgerr); if (tmpgerr) { g_propagate_prefixed_error(gerr, tmpgerr, "stat_cache_delete_parent: no parent path"); } @@ -748,8 +888,11 @@ int stat_cache_enumerate(stat_cache_t *cache, const char *path_prefix, void (*f) while ((entry = stat_cache_iter_current(iter))) { log_print(LOG_DEBUG, SECTION_STATCACHE_ITER, "key: %s", entry->key); log_print(LOG_DEBUG, SECTION_STATCACHE_ITER, "fn: %s", entry->key + (iter->key_prefix_len - 1)); - f(path_prefix, entry->key + (iter->key_prefix_len - 1), user); - ++found_entries; + // Ignore negative (non-existent) entries, those tagged with st_mode == 0 + if (entry->value->st.st_mode != 0) { + f(path_prefix, entry->key + (iter->key_prefix_len - 1), user); + ++found_entries; + } free(entry); stat_cache_iter_next(iter); } @@ -765,6 +908,7 @@ int stat_cache_enumerate(stat_cache_t *cache, const char *path_prefix, void (*f) void stat_cache_walk(void) { leveldb_readoptions_t *roptions; struct leveldb_iterator_t *iter; + const struct stat_cache_value *itervalue; log_print(LOG_NOTICE, SECTION_STATCACHE_CACHE, "stat_cache_walk: starting: %p", gcache); @@ -773,9 +917,14 @@ void stat_cache_walk(void) { iter = leveldb_create_iterator(gcache, roptions); // We've kept a pointer to cache for just this call leveldb_iter_seek_to_first(iter); for (; leveldb_iter_valid(iter); leveldb_iter_next(iter)) { - size_t klen; + size_t klen, vlen; + bool negative_entry; + char posneg[] = "positive"; const char *iterkey = leveldb_iter_key(iter, &klen); - log_print(LOG_NOTICE, SECTION_STATCACHE_CACHE, "stat_cache_walk: iterkey = %s", iterkey); + itervalue = (const struct stat_cache_value *) leveldb_iter_value(iter, &vlen); + negative_entry = stat_cache_is_negative_entry(*itervalue); + if (negative_entry) strcpy(posneg, "negative"); + log_print(LOG_NOTICE, SECTION_STATCACHE_CACHE, "stat_cache_walk: iterkey = %s :: posneg: %s", iterkey, posneg); } leveldb_iter_destroy(iter); leveldb_readoptions_destroy(roptions); @@ -789,12 +938,15 @@ bool stat_cache_dir_has_child(stat_cache_t *cache, const char *path) { BUMP(statcache_has_child); - log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "stat_cache_dir_has_children(%s)", path); + log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "stat_cache_dir_has_child(%s)", path); iter = stat_cache_iter_init(cache, path); if ((entry = stat_cache_iter_current(iter))) { - has_children = true; - log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "stat_cache_dir_has_children(%s); entry \'%s\'", path, entry->key); + // Ignore negative (non-existent) entries, those tagged with st_mode == 0 + if (entry->value->st.st_mode != 0) { + has_children = true; + log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "stat_cache_dir_has_child(%s); entry \'%s\'", path, entry->key); + } free(entry); } stat_cache_iterator_free(iter); @@ -807,22 +959,30 @@ void stat_cache_delete_older(stat_cache_t *cache, const char *path_prefix, unsig struct stat_cache_entry *entry; GError *tmpgerr = NULL; unsigned int deleted_entries = 0; + struct stat_cache_value value; BUMP(statcache_delete_older); + // Zero-out structure; some fields we don't populate but want to be 0, e.g. st_atim.tv_nsec + memset(&value, 0, sizeof(struct stat_cache_value)); + log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "stat_cache_delete_older: %s", path_prefix); iter = stat_cache_iter_init(cache, path_prefix); while ((entry = stat_cache_iter_current(iter))) { - log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "stat_cache_delete_older: %s: min_gen %lu: loc_gen %lu", - entry->key, minimum_local_generation, entry->value->local_generation); - if (entry->value->local_generation < minimum_local_generation) { - stat_cache_delete(cache, key2path(entry->key), &tmpgerr); - ++deleted_entries; - if (tmpgerr) { - g_propagate_prefixed_error(gerr, tmpgerr, "stat_cache_delete_older: "); - free(entry); - stat_cache_iterator_free(iter); - return; + // Not deleting, rather, inserting negative entries. + if (entry->value->st.st_mode != 0) { + log_print(LOG_DEBUG, SECTION_STATCACHE_CACHE, "stat_cache_delete_older: %s: min_gen %lu: loc_gen %lu", + entry->key, minimum_local_generation, entry->value->local_generation); + if (entry->value->local_generation < minimum_local_generation) { + stat_cache_negative_set(&value); + stat_cache_value_set(cache, key2path(entry->key), &value, &tmpgerr); + if (tmpgerr) { + g_propagate_prefixed_error(gerr, tmpgerr, "stat_cache_delete_older: "); + free(entry); + stat_cache_iterator_free(iter); + return; + } + ++deleted_entries; } } free(entry); @@ -1006,7 +1166,8 @@ void stat_cache_prune(stat_cache_t *cache, bool first) { } if (bloomfilter_exists(boptions, parentpath, strlen(parentpath))) { - log_print(LOG_DEBUG, SECTION_STATCACHE_PRUNE, "stat_cache_prune: exists in bloom filter\'%s\'", parentpath); + log_print(LOG_DEBUG, SECTION_STATCACHE_PRUNE, + "stat_cache_prune: parent exists in bloom filter\'%s\'", parentpath); // If the parent is in the filter, and this child is a directory, add it to // the filter for iteration at the next depth if (S_ISDIR(itervalue->st.st_mode)) { @@ -1021,16 +1182,20 @@ void stat_cache_prune(stat_cache_t *cache, bool first) { // Future version of fusedav will have negative entries. If we revert a binding back, get rid // of these negative entries on startup, so as not to confuse this version of fusedav if (first && itervalue->st.st_mode == 0) { - log_print(LOG_INFO, SECTION_STATCACHE_PRUNE, "stat_cache_prune: deleting negative entry \'%s\'", path); + log_print(LOG_NOTICE, SECTION_STATCACHE_PRUNE, "stat_cache_prune: deleting negative entry \'%s\'", path); stat_cache_delete(cache, path, NULL); } } } else { - log_print(LOG_DEBUG, SECTION_STATCACHE_PRUNE, "stat_cache_prune: doesn't exist in bloom filter \'%s\'", parentpath); + struct stat_cache_value value; + // Zero-out structure; some fields we don't populate but want to be 0, e.g. st_atim.tv_nsec + memset(&value, 0, sizeof(struct stat_cache_value)); + log_print(LOG_NOTICE, SECTION_STATCACHE_PRUNE, "stat_cache_prune: doesn't exist in bloom filter \'%s\'", parentpath); ++deleted_entries; - log_print(LOG_INFO, SECTION_STATCACHE_PRUNE, "stat_cache_prune: deleting \'%s\'", path); - stat_cache_delete(cache, path, NULL); + log_print(LOG_NOTICE, SECTION_STATCACHE_PRUNE, "stat_cache_prune: setting negative entry \'%s\'", path); + stat_cache_negative_set(&value); + stat_cache_value_set(cache, path, &value, NULL); } free(parentpath); } @@ -1095,10 +1260,11 @@ void stat_cache_prune(stat_cache_t *cache, bool first) { log_print(LOG_DEBUG, SECTION_STATCACHE_PRUNE, "stat_cache_prune: exists in bloom filter (basepath of)\'%s\'", iterkey); } else { - log_print(LOG_DEBUG, SECTION_STATCACHE_PRUNE, "stat_cache_prune: updated_children: deleting \'%s\'", iterkey); + log_print(LOG_NOTICE, SECTION_STATCACHE_PRUNE, "stat_cache_prune: updated_children: deleting \'%s\'", iterkey); ++deleted_entries; // We recreate the basics of stat_cache_delete here, since we can't call it directly // since it doesn't deal with keys with "updated_children:" + // NB. We are not imitating stat_cache_negative_entry here woptions = leveldb_writeoptions_create(); leveldb_delete(cache, woptions, iterkey, strlen(iterkey) + 1, &errptr); leveldb_writeoptions_destroy(woptions); diff --git a/src/statcache.h b/src/statcache.h index fa2f09e2..e139a828 100644 --- a/src/statcache.h +++ b/src/statcache.h @@ -53,16 +53,17 @@ struct stat_cache_iterator { size_t key_prefix_len; }; +// For values which exist in the cache struct stat_cache_value { struct stat st; unsigned long local_generation; time_t updated; - bool prepopulated; // Added to the local cache; not from the server. + bool from_propfind; // A propfind caused this update char remote_generation[RGEN_LEN]; }; void stat_cache_print_stats(void); -int print_stat(struct stat *stbuf, const char *title); +int print_stat(struct stat *stbuf, const char *title, const char *path); unsigned long stat_cache_get_local_generation(void); @@ -75,12 +76,17 @@ time_t stat_cache_read_updated_children(stat_cache_t *cache, const char *path, G void stat_cache_value_set(stat_cache_t *cache, const char *path, struct stat_cache_value *value, GError **gerr); void stat_cache_value_free(struct stat_cache_value *value); +bool stat_cache_is_negative_entry(struct stat_cache_value value); +void stat_cache_negative_set(struct stat_cache_value *value); +time_t stat_cache_next_propfind(struct stat_cache_value value); +void stat_cache_from_propfind(struct stat_cache_value *value, bool bvalue); void stat_cache_delete(stat_cache_t *cache, const char* path, GError **gerr); void stat_cache_delete_parent(stat_cache_t *cache, const char *path, GError **gerr); void stat_cache_delete_older(stat_cache_t *cache, const char *key_prefix, unsigned long minimum_local_generation, GError **gerr); void stat_cache_walk(void); -int stat_cache_enumerate(stat_cache_t *cache, const char *key_prefix, void (*f) (const char *path_prefix, const char *filename, void *user), void *user, bool force); +int stat_cache_enumerate(stat_cache_t *cache, const char *key_prefix, void (*f) (const char *path_prefix, + const char *filename, void *user), void *user, bool force); bool stat_cache_dir_has_child(stat_cache_t *cache, const char *path); void stat_cache_prune(stat_cache_t *cache, bool first); diff --git a/tests/Makefile b/tests/Makefile index 1c334822..a0087eee 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,10 +1,16 @@ testdir = /root/pantheon/fusedav/tests +curdir := $(shell pwd) cltest = $(testdir)/cltest.sh # usage on Makefile line: 'cltest-flags=-i64' # -i is iters; default is 64 cltest-flags= +urlencode = $(testdir)/urlencode.sh +# usage on Makefile line: 'urlencode-flags=-i64' +# -i is iters; default is 1 +urlencode-flags= + statcacheprune = $(testdir)/statcacheprune.sh # usage on Makefile line: 'statcacheprune-flags=-d2 -f64 -tdflt' # default for -d is 2; max allowed is 4; for -f it's 64 @@ -24,6 +30,21 @@ dutest = $(testdir)/dutest.sh # -v for verbose dutest-flags= +# Create a lot of files in a lot of directories +writefiles-create = $(testdir)/writefiles-create.sh +# -v for verbose +writefiles-create-flags= + +# Remove a lot of files in a lot of directories +writefiles-remove = $(testdir)/writefiles-remove.sh +# -v for verbose +writefiles-remove-flags= + +# Write some files, delete some of them, check for correct positive/negative entries +write-delete-checknegative = $(testdir)/write-delete-checknegative.sh +# -v for verbose +write-delete-checknegative-flags= + # NB: no PASS/FAIL notification yet from iozone, still requires eyeballs iozone = /root/pantheon/iozone/src/current/iozone # -g: start with files of this size (by default 64K) @@ -52,6 +73,10 @@ continualwrites = $(testdir)/continualwrites # flag -v for verbose, -f# for number of files 'continualwrites-flags=-v -w -f16' continualwrites-flags = +rename = $(testdir)/rename +# flag -v for verbose 'rename-flags=-v' +rename-flags = + one-open-many-writes = $(testdir)/one-open-many-writes # flag -v for verbose, -f# for number of files 'one-open-many-writes=-v -w -f16' one-open-many-writes-flags = @@ -62,9 +87,14 @@ forensichaventest-flags = trunc = $(testdir)/trunc # -v for verbose, -r# for number of rounds, -f# for number of files, -s# for size of write, -i# for -# number of write iters'trunc-flags=-v -r 8 -f 8 -s 32 -i 16' +# number of write iters 'trunc-flags=-v -r 8 -f 8 -s 32 -i 16' trunc-flags = +pfbackoff = $(testdir)/pfbackoff.sh +# -v for verbose, -n# for number of rounds +# number of iters 'pfbackoff-flags=-v -n 8' +pfbackoff-flags = + saintmode-haproxy = $(testdir)/saintmode-haproxy.sh # -v for verbose, -i# for number of write iters'saintmode-haproxy-flags=-v -i 16' saintmode-haproxy-flags = @@ -109,24 +139,32 @@ forensic-haven-cleanup = $(testdir)/forensic-haven-cleanup.sh # -v for verbose, 'forensic-haven-cleanup-flags=-v' forensic-haven-cleanup-flags = -all: run-simple-stress-tests +all: run-simple-stress-tests run-nonfiles-tests +# S +# # restrict unit tests to low-resource tests -run-unit-tests: echo-start run-cltest run-statcacheprune run-dutest run-readrecursive run-readwhatwaswritten-unit run-readwhatwaswritten-unlink-unit run-continualwrites-unit run-trunc run-forensic-haven-cleanup run-iozone-unit echo-finish +run-unit-tests: echo-start run-cltest run-urlencode run-statcacheprune run-dutest run-writefiles-create run-readrecursive run-writefiles-remove run-readwhatwaswritten-unit run-readwhatwaswritten-unlink-unit run-continualwrites-unit run-rename run-trunc run-iozone-unit echo-finish # stress test is all test, but run the stress version if there is both a unit and a stress version. e.g. iozone -run-stress-tests: echo-start run-cltest run-statcacheprune run-dutest run-readrecursive run-readwhatwaswritten-stress run-readwhatwaswritten-unlink-stress run-continualwrites-unit run-trunc run-forensic-haven-cleanup run-iozone-stress run-saintmode-haproxy run-saintmode-nginx run-unhealthy-haproxy run-saintmode-writes-nginx echo-finish +run-stress-tests: echo-start run-cltest run-urlencode run-statcacheprune run-dutest run-writefiles-create run-readrecursive run-writefiles-remove run-readwhatwaswritten-stress run-readwhatwaswritten-unlink-stress run-continualwrites-unit run-rename run-trunc run-iozone-stress run-saintmode-haproxy run-saintmode-nginx run-unhealthy-haproxy run-saintmode-writes-nginx echo-finish -# simple stress test; don't run the 'error' tests since they assume a onebox for creating error conditions which don't occur on yolo endpoint -run-simple-stress-tests: echo-finish run-cltest run-statcacheprune run-dutest run-readrecursive run-readwhatwaswritten-stress run-readwhatwaswritten-unlink-stress run-continualwrites-unit run-trunc run-forensic-haven-cleanup run-iozone-stress echo-finish +# simple stress test; don't run the 'saintmode' tests since they assume a unified development environment for creating error conditions which don't generally occur in production environments. +run-simple-stress-tests: echo-start run-cltest run-urlencode run-statcacheprune run-dutest run-writefiles-create run-readrecursive run-writefiles-remove run-readwhatwaswritten-stress run-readwhatwaswritten-unlink-stress run-continualwrites-unit run-rename run-trunc run-iozone-stress echo-finish + +# I'm having difficulties running some tests in /files, and some just in . Separate these and +# handle on the command line. If someone can figure out the issues switching over file system boundaries, +# please make this better. For now, I run, from : +# make -f /root/pantheon/fusedav/tests/Makefile run-nonfiles-tests; (cd files; make -f /root/pantheon/fusedav/tests/Makefile) +run-nonfiles-tests: echo-start run-pfbackoff run-write-delete-checknegative run-forensic-haven-cleanup echo-finish # stress test is all test, but run the stress version if there is both a unit and a stress version. e.g. iozone # unlink test is creating increasing numbers of open files which never get closed. Remove for now. -# run-continual-tests: run-cltest run-statcacheprune run-readrecursive run-readwhatwaswritten-stress run-readwhatwaswritten-unlink-stress run-trunc run-iozone-continual -run-continual-tests: echo-start run-cltest run-statcacheprune run-dutest run-readrecursive run-readwhatwaswritten-stress run-readwhatwaswritten-unlink-stress run-trunc run-forensic-haven-cleanup run-iozone-continual echo-finish +# run-continual-tests: run-cltest run-urlencode run-statcacheprune run-writefiles-create run-readrecursive run-writefiles-remove run-readwhatwaswritten-stress run-readwhatwaswritten-unlink-stress run-trunc run-iozone-continual +run-continual-tests: echo-start run-cltest run-urlencode run-statcacheprune run-dutest run-writefiles-create run-readrecursive run-writefiles-remove run-readwhatwaswritten-stress run-readwhatwaswritten-unlink-stress run-trunc run-iozone-continual echo-finish # short stress test is all tests but iozone, but run the stress version if there is both a unit and a stress version. e.g. iozone -run-short-stress-tests: echo-start run-cltest run-statcacheprune run-dutest run-readrecursive run-readwhatwaswritten-stress run-readwhatwaswritten-unlink-stress run-trunc echo-finish +run-short-stress-tests: echo-start run-cltest run-urlencode run-statcacheprune run-dutest run-writefiles-create run-readrecursive run-writefiles-remove run-readwhatwaswritten-stress run-readwhatwaswritten-unlink-stress run-continualwrites-unit run-rename run-trunc echo-finish # run this to test the system under error. To do this: # use the fusedav client inject error mechanism. Use the writewrite_test @@ -147,20 +185,43 @@ run-write-errors: run-continualwrites-unit run-forensic-haven-test: run-forensic-haven-unit echo-start: - @ echo "Start: "; date + @ echo "Start: "; date; echo "Current Directory: $(curdir)" +.PHONY: echo-finish echo-finish: @ echo "Finish: "; date run-cltest: $(cltest) $(cltest-flags) +run-urlencode: + $(urlencode) $(urlencode-flags) + run-statcacheprune: $(statcacheprune) $(statcacheprune-dirs) $(statcacheprune-flags) run-dutest: $(dutest) $(dutest-flags) +run-writefiles-create: + $(writefiles-create) $(writefiles-create-flags) + +run-writefiles-remove: + $(writefiles-remove) $(writefiles-remove-flags) + +.PHONY: run-write-delete-checknegative +run-write-delete-checknegative: + $(write-delete-checknegative) $(write-delete-checknegative-flags) + +.PHONY: run-forensic-haven-cleanup +run-forensic-haven-cleanup: + $(forensic-haven-cleanup) $(forensic-haven-flags) + +# Seems .PHONY is needed for this one; not sure why +.PHONY: run-pfbackoff +run-pfbackoff: + $(pfbackoff) $(pfbackoff-flags) + run-iozone-unit: $(iozone) -Ra $(iozone-unit-flags) @@ -203,6 +264,12 @@ run-continualwrites-unit: $(continualwrites) $(continualwrites): $(testdir)/continualwrites.c cc $< -std=c99 -g -o $@ +run-rename: $(rename) + $(rename) $(rename-flags) + +$(rename): $(testdir)/rename.c + cc $< -std=c99 -g -o $@ + run-one-open-many-writes-unit: $(one-open-many-writes) $(one-open-many-writes) $(one-open-many-writes-flags) @@ -249,6 +316,3 @@ run-perfanalysis-read: $(perfanalysis-read) $(perfanalysis-read): $(testdir)/perfanalysis-writeread.c cc $< -std=c99 -g -o $@ - -run-forensic-haven-cleanup: - $(forensic-haven-cleanup) $(forensic-haven-flags) diff --git a/tests/README b/tests/README new file mode 100644 index 00000000..851d79a0 --- /dev/null +++ b/tests/README @@ -0,0 +1,159 @@ +Setup: +Install the fusedav repository from github +If you install it in: /root/pantheon/fusedav; + then will be "/root/pantheon/fusedav/tests" + +NB. If you install it elsewhere, you may need to add +"testdir=" to the call to make, since some tests use that variable +to find other tests. + +A. The Basic Tests: + +1. The Regular Version: (will run "run-simple-stress-tests") +cd to a files directory, e.g. /srv/bindings//files +execute: make -f /Makefile + +2. The Short Version: (will run "run-unit-tests") +These are roughly the same set of tests as the Basic version above; +however, some of the tests are run with parameters to make them +do less work and finish more quickly. +cd to a files directory, e.g. /srv/bindings//files +execute: make -f /Makefile run-unit-tests + +3. The Long Version: (will run "run-stress-tests") +These are a superset of the Basic Tests, they include the "saintmode" tests. +These saintmode tests really only work in certain specially-configured, unified +development environments, since they attempt to disable various functionality +on the path to the fileserver, i.e. nginx and haproxy. +cd to a files directory, e.g. /srv/bindings//files +execute: make -f /Makefile run-stress-tests + +4. The Truncated Stress Version: (will run "run-short-stress-tests") +Same as #3, but omits iozone in order to get a shorter execution time. + +Individual Tests in the above suites: +cltest + - command line tests, cp, mv, rm, etc. +urlencode + - cltest, except that the filenames get coded to ensure we can urlencode correctly +statcacheprune + - creates a bunch of directories and files + - designed as stress test, run simulaneously on multiple bindings with differnt flags + which will create different file names on different fusedav bindings, and test + the ability to not get confused, and to keep up + - not sure why it's called statcacheprune, which is something rather different + - Previously used to trigger "Too Many Events" exception +dutest + - Regression test to check for zero-length files. + - Inadequate as tests because it has to hardcode the files it expects + to have zero-length +iozone + - Third party file system performance test. + - Mostly used as test for file system correctness, and for stress. + - The version we run has a slight modification, since there is one + POSIX feature we don't support +readrecursive + - Reads all files in the file system +openunlinkclose + - I forget what it's called, but this tests writing to a bare file descriptor +readwhatwaswritten + - write some files, read them back and ensure the content read is the same as was written +continualwrites + - Similar to readwhatwaswritten, but is designed to tally file system errors. + - Can be run in conjunction with fusedav error injection + - See instructions in continualwrites.c for more +rename + - Test renaming files +one-open-many-writes + - Hmm. "...derived from continualwrites.c. In the middle of the test, + stop the fileserver and see what happens." +forensichaventest + - Run in conjunction with test errors to ensure error'ed writes + go to the forensic haven +trunc + - tests truncating files on open, I think +pfbackoff + - test that non-existent files properly cause or avoid propfinds depending on + how many times they have been tried +saintmode-haproxy + - In a unified environment, partially or fully disables the fileserver in the + middle of the test by messing with haproxy. Tests that saintmode works correctly. +saintmode-nginx + - In a unified environment, partially or fully disables the fileserver in the + middle of the test by messing with nginx. Tests that saintmode works correctly. +saintmode-writes-nginx + - Checks that writes and removes fail when in saintmode +unhealthy-haproxy + - runs the short version of the basic tests (make -f Makefile above) while taking + selectively disabling haproxy. Perhaps a forerunner of saintmode-haproxy? + - Needs work to actually run +timestamptest + - Requires a binding id to be passed in + - Run on two fusedav bindings simultaneously +perfanalysis-write +perfanalysis-read + - See perfanalysis-writeread.c in Other Tests below +forensic-haven-cleanup + - The forensic haven needs to restrain the number of files it contains. + - This tests a number of scenarios to ensure that files are deleted as needed +forensic-haven-test + - run this test in conjunction with filecache_forensic_haven_test in + util.c while injecting errors (make INJECT_ERRORS=1) + - Run overnight or so; if there are no catastrophic failures (e.g. segv) + consider it a pass. + - NB: this test needs to create an errors file in <...>/files directory + but often fails because we are injecting errors. It may need to be + started several times before it actually takes. If the errors file + cannot be created, the program exits. + +B. Other Tests +1. continualtest.sh + Calls the Makefile in a loop and runs "run-continual-tests" set of tests. + It dumps heap and compares memory growth over time (hours, days, ...) + This was used when fusedav had a memory leak via curl and the nsf library +2. dual-binding-tests.sh + dual-binding-test.sh + basic-ops.sh + A somewhat failed experiment to design a flexible mechanism to + run tests against environments with multiple fusedav bindings. + The script dual-binding-test.sh can take different programs/scripts + one for each binding, and pass in some parameters to them. + The script dual-binding-tests.sh (tests plural) calls + dual-binding-test.sh (test singular), passing in basic-ops.sh + as the script for both, and runs a series of tests. +3. perfanalysis-writeread.c + Tests how long it takes before a write on one fusedav binding + is available on a different fusedav binding on the same environment. + One binding runs the program in write mode, the other in read mode. + These can be run via "make -f Makefile. + Somewhat complicated to run. +4. "gerror" tests + Not part of the tests directory itself. + After "make clean" on fusedav binary, recompile with: + "make INJECT_ERRORS=1" + Configure fusedav/src/util.c with the errors you want to simulate. + It will inject errors depending on the configuration, to test + the "gerror" mechanism. + Since it is hard to configure a system to cause the errors you + might want to track, and to ensure that the error reporting did + not itself introduce errors (Don't you hate it when reporting + an error causes the program to crash due to an errant pointer?) + this was originally created to inject errors to trigger those + gerror messages. + Somewhat complicated to run. See fusedav/src/util.c for + instructions. + +C. Other Tests and Miscellaneous Scripts +1. create-many-files.sh + Creates many files in the current directory. + Not part of testing, but can be used to create many files before testing + Often used to test propfind performance if a directory has many files +2. migrate-cassandra-data.sh + Not sure why it's here +3. test_fusedav_stress.py + A python mechanism for invoking the Basic Test, Regular Version + Not used +4. test_dav.py + Canonical unit tests called from python + Not used + diff --git a/tests/basic-ops.sh b/tests/basic-ops.sh new file mode 100755 index 00000000..5a8a8b50 --- /dev/null +++ b/tests/basic-ops.sh @@ -0,0 +1,311 @@ +#! /bin/bash +set +e + +echo "ENTER: basic-ops.sh $@" +usage() +{ +cat << EOF +usage: $0 options + +This script runs the basic ls test (for the dual-bindings.sh test) + +OPTIONS: + -h Show this message + -i Number of iterations + -b Binding dir and id + -c Command line args + -r Add -R to ls + -v Verbose +EOF +} + +iters=0 +verbose=0 +binding="none" +basedir="basic-ops" +testdir="basic-ops-dir" +dir=$basedir/$testdir +file="basic-ops-file" +cmd="none" +declare -i ret + +while getopts "hri:b:c:d:v" OPTION +do + case $OPTION in + h) + usage + exit 1 + ;; + i) + iters=$OPTARG + ;; + b) + binding=$OPTARG + ;; + c) + cmd=$OPTARG + ;; + d) + dir=$OPTARG + ;; + v) + verbose=1 + ;; + ?) + usage + exit + ;; + esac +done + +if [ $binding == "none" ]; then + echo "Binding (with dir and id) is required. Exiting ..." + exit +fi + +if [ $iters -eq 0 ]; then + iters=1 +fi + +list() { + echo "list: ls -al $1/$2" + ls -al $1/$2 + ret=$? + return $ret +} + +listR() { + echo "listR: ls -alR $1/$2" + ls -alR $1/$2 + ret=$? + return $ret +} + +makedir() { + echo "makedir: mkdir $1/$2" + if [ ! -d $1 ]; then + mkdir $1 + fi + mkdir $1/$2 + ret=$? + return $ret +} + +readfile() { + echo "read: cat $$file > /dev/null" + cat $file > /dev/null + ret=$? + return $ret +} + +writefile() { + echo "writefile: head -c 67 /dev/urandom > $1/$2/$3-$4" + head -c 67 /dev/urandom > $1/$2/$3-$4 + ret=$? + return $ret +} + +removedir() { + echo "removedir: rmdir $1/$2" + rmdir $1/$2 + ret=$? + return $ret +} + +removefiles() { + echo "removefiles: rm -f $1/$2/*" + rm -f $1/$2/* + ret=$? + return $ret +} + +removefile() { + echo "removefiles: rm -f $1/$2/$3-$4" + rm $1/$2/$3-$4 + ret=$? + return $ret +} + +insertsleep() { + sleep $1 + ret=$? + return $ret +} + +# get commands from string and put in array +commands=(${cmd//;/ }) +if [ $verbose -eq 1 ]; then + echo "COMMANDS: $cmd :: ${commands[*]}" +fi + +pass=0 +fail=0 + +if [ $verbose -eq 1 ]; then + starttime=$(date +%s) +fi + +for cmd in "${commands[@]}"; do + result=0 + expect_failure=0 + if [ $cmd == "list" ]; then + list $binding $dir + ret=$? + elif [ $cmd == "listbase" ]; then + list $binding $basedir + ret=$? + elif [ $cmd == "flist" ]; then + expect_failure=1 + echo "--Expect Failure" + list $binding $dir + ret=$? + elif [ $cmd == "listR" ]; then + listR $binding $dir + ret=$? + elif [ $cmd == "listRbase" ]; then + listR $binding $basedir + ret=$? + elif [ $cmd == "flistR" ]; then + expect_failure=1 + echo "--Expect Failure" + listR $binding $dir + ret=$? + elif [ $cmd == "makedir" ]; then + makedir $binding $dir + ret=$? + elif [ $cmd == "fmakedir" ]; then + expect_failure=1 + echo "--Expect Failure" + makedir $binding $dir + ret=$? + elif [ $cmd == "writefile" ]; then + fail=0 + pass=0 + iter=1 + while [ $iter -le $iters ] + do + echo "iter: $iter" + writefile $binding $dir $file $iter + ret=$? + # We will also increment once more below, so one of the values will be off by one. + # Am I worried? + if [ "$ret" -ne 0 ]; then + fail=$((fail + 1)) + else + pass=$((pass + 1)) + fi + iter=$((iter + 1)) + done + elif [ $cmd == "fwritefile" ]; then + expect_failure=1 + echo "--Expect Failure" + fail=0 + pass=0 + while [ $iter -le $iters ] + do + echo "iter: $iter" + writefile $binding $dir $file $iter + ret=$? + # We will also increment once more below, so one of the values will be off by one. + # Am I worried? + # Reverse meaning of ret + if [ "$ret" -eq 0 ]; then + fail=$((fail + 1)) + else + pass=$((pass + 1)) + fi + iter=$((iter + 1)) + done + elif [ $cmd == "readfile" ]; then + for file in $binding/$dir; do + readfile $file + ret=$? + # We will also increment once more below, so one of the values will be off by one. + # Am I worried? + if [ "$ret" -ne 0 ]; then + fail=$((fail + 1)) + else + pass=$((pass + 1)) + fi + done + elif [ $cmd == "freadfile" ]; then + expect_failure=1 + echo "--Expect Failure" + for file in $binding/$dir; do + readfile $file + ret=$? + # We will also increment once more below, so one of the values will be off by one. + # Am I worried? + # Reverse meaning of ret + if [ "$ret" -eq 0 ]; then + fail=$((fail + 1)) + else + pass=$((pass + 1)) + fi + done + elif [ $cmd == "removefiles" ]; then + removefiles $binding $dir + ret=$? + elif [ $cmd == "fremovefiles" ]; then + expect_failure=1 + echo "--Expect Failure" + removefiles $binding $dir + ret=$? + elif [ $cmd == "removefile" ]; then + removefile $binding $dir $file 1 + ret=$? + elif [ $cmd == "fremovefile" ]; then + expect_failure=1 + echo "--Expect Failure" + removefile $binding $dir $file 1 + ret=$? + elif [ $cmd == "removedir" ]; then + removedir $binding $dir + ret=$? + elif [ $cmd == "fremovedir" ]; then + expect_failure=1 + echo "--Expect Failure" + removedir $binding $dir + ret=$? + elif [ $cmd == "sleep" ]; then + insertsleep 3 + ret=$? + else + echo "Unknowned command: $cmd" + ret=0 + fi + + # If this is an expect failure call, reverse meaning of ret + if [ $expect_failure -ne 0 ]; then + if [ $ret -eq 0 ]; then + ret=1 + else + ret=0 + fi + echo "EXPECTED FAILURE: $ret" + fi + + if [ "$ret" -ne 0 ]; then + fail=$((fail + 1)) + echo "FAIL" + else + pass=$((pass + 1)) + echo "PASS" + fi +done + + +if [ $verbose -eq 1 ]; then + endtime=$(date +%s) + elapsedtime=$(( $endtime - $starttime )) + echo "Elapsed time: $elapsedtime" +fi + +if [ $fail -ne 0 ]; then + echo "FAIL: times failed $fail; files passed $pass" +else + echo "PASS: times passed $pass" +fi + +echo "EXIT: basic-ops.sh" +exit $fail diff --git a/tests/cltest.sh b/tests/cltest.sh index eda7178d..4d4dd4df 100755 --- a/tests/cltest.sh +++ b/tests/cltest.sh @@ -63,6 +63,7 @@ if [ $verbose -eq 1 ]; then starttime=$(date +%s) fi +echo "Current directory: $(pwd)" # append; this also creates a file for later tests rm cltest.file > /dev/null 2>&1 rm cltest.file.save > /dev/null 2>&1 diff --git a/tests/cltest2.sh b/tests/cltest2.sh index f949650b..9c356b1f 100755 --- a/tests/cltest2.sh +++ b/tests/cltest2.sh @@ -1,6 +1,8 @@ #! /bin/bash set +e +# Obsolete. A better version exists as cltest.sh + start=1 # Skip to test $start, which can be passed as a parameter if [ $# -gt 0 ] @@ -11,7 +13,7 @@ fi starttime=$(date +%s) iters=1 - jters=1 + jters=10000 # append; this also creates a file for later tests test=1 rm -f cltest2.file > /dev/null 2>&1 diff --git a/tests/dual-binding-create.sh b/tests/dual-binding-create.sh new file mode 100755 index 00000000..d76163c7 --- /dev/null +++ b/tests/dual-binding-create.sh @@ -0,0 +1,187 @@ +#! /bin/bash +set +e + +usage() +{ +cat << EOF +usage: $0 options + +This script tests that a file created on one binding is immediately available when accessed +on a different binding in the environment. + +OPTIONS: + -h Show this message + -i Number of iterations + -v Verbose + -1 First binding id + -2 Second binding id +EOF +} + +iters=6 +verbose=0 +vverbose=0 +b1="none" +b2="none" +pass=0 +fail=0 + +echo "ENTER dual-binding-write" + +while getopts "hi:1:2:v" OPTION +do + case $OPTION in + h) + usage + exit 1 + ;; + i) + iters=$OPTARG + ;; + 1) + b1=$OPTARG + ;; + 2) + b2=$OPTARG + ;; + v) + verbose=1 + ;; + w) + vverbose=1 + ;; + ?) + usage + exit + ;; + esac +done + +if [ $b1 == "none" ]; then + echo "Binding 1 id is required. Exiting ..." + exit +fi + +if [ $b2 == "none" ]; then + echo "Binding 2 id is required. Exiting ..." + exit +fi + +bid1path=/srv/bindings/$b1 +bid2path=/srv/bindings/$b2 + +# directory for the file +filedir=files/dual-binding-create-dir +# name of file being created +createdfile=$filedir/dual-binding-create.file + +# The fibonacci sequence; if fewer than this many seconds have passed +# since last ENOENT propfind on the missing file, no propfind is generated +fibs=(0 1 2 3 5 8 13 21 34 55 89 144 233 377 610) + +if [ $verbose -eq 1 ]; then + starttime=$(date +%s) +fi + +#### Set up +# Cleanup whatever might be in there +rm -f $bid1path/$filedir/* +rmdir $bid1path/$filedir + +# Reset the state of the files to an initial state +if [ $verbose -eq 1 ]; then + btool invalidate srv-bindings-$b1-files.mount +else + btool invalidate srv-bindings-$b1-files.mount > /dev/null 2>&1 +fi + +# make the directory for the new file and put a seed file there +# (just to have a non-empty directory) +mkdir $bid1path/$filedir + +iter=0 +while [ $iter -lt $iters ] +do + sleeptime=${fibs[$iter]} + + if [ $verbose -eq 1 ]; then + echo "Sleeping $sleeptime seconds" + fi + sleep $sleeptime + + if [ $verbose -eq 1 ]; then + echo "$iter: cat $bid1path/$createdfile" + date '+%Y-%m-%d %H:%M:%S' + cat $bid1path/$createdfile + else + # We haven't created the file yet, so access should trigger fibonacci backoff + cat $bid1path/$createdfile > /dev/null 2>&1 + fi + + iter=$((iter + 1)) +done + +# Create file on binding two +# Binding one is now in fibonacci backoff for 8 seconds, create file on bid2 +echo "createdfile" > $bid2path/$createdfile + +# Access file on binding one; won't be found for 8 seconds +# We only increment on propfinds returning ENOENT +# We did 5 iters above, on the 5th, the backoff window gets set to 8 + +for iter in 1 2 3; do + if [ $verbose -eq 1 ]; then + echo "$iter: cat $bid1path/$createdfile should fail" + date '+%Y-%m-%d %H:%M:%S' + fi + + if [ $verbose -eq 1 ]; then + cat $bid1path/$createdfile + else + cat $bid1path/$createdfile > /dev/null 2>&1 + fi + # Still in fibonacci backoff window, so should fail, since this binding doesn't think the file exists + if [ $? -eq 0 ]; then + fail=$((fail + 1)) + echo "FAIL" + else + pass=$((pass + 1)) + echo "PASS" + fi + + # Sleep to use up part of the fibonacci backoff window + sleep 3 +done + +if [ $verbose -eq 1 ]; then + echo "$iter: cat $bid1path/$createdfile should succeed" + date '+%Y-%m-%d %H:%M:%S' +fi + +# We should have slept 9 seconds and have slipped out of the fibonacci backoff window. +# Expect this access to trigger a propfind, and return success on fail access +if [ $verbose -eq 1 ]; then + cat $bid1path/$createdfile +else + cat $bid1path/$createdfile > /dev/null 2>&1 +fi +if [ $? -ne 0 ]; then + fail=$((fail + 1)) + echo "FAIL" +else + pass=$((pass + 1)) + echo "PASS" +fi + +if [ $verbose -eq 1 ]; then + endtime=$(date +%s) + elapsedtime=$(( $endtime - $starttime )) + echo "Elapsed time: $elapsedtime" +fi + +if [ $fail -ne 0 ]; then + echo "FAIL: attempts failed $fail; attempts passed $pass" +else + echo "PASS: attempts passed $pass" +fi + diff --git a/tests/dual-binding-test.sh b/tests/dual-binding-test.sh new file mode 100755 index 00000000..9def836e --- /dev/null +++ b/tests/dual-binding-test.sh @@ -0,0 +1,149 @@ +#! /bin/bash +set +e + +usage() +{ +cat << EOF +usage: $0 options + +This script enables dual-binding tests + +OPTIONS: + -h Show this message + -i Number of iterations + -v Verbose + -1 First binding id + -2 Second binding id + -a First program + -b Second program + -c First program command line args + -d Second program command line args + Example: /root/pantheon/fusedav/tests/dual-binding-tests.sh -1 32fffe7694044da1be9c41cfd489185d -2 975b26cec42a4c1aa7976ad941aa54f9 -a basic-ops.sh -b basic-ops.sh -v -c "-v -c write" -d "-v -c read" +EOF +} + +iters=0 +verbose=0 +b1="none" +b2="none" +pa="none" +pb="none" +ca="none" +cb="none" + +echo "ENTER dual-binding-test" + +scriptdir=/root/pantheon/fusedav/tests + +while getopts "hi:1:2:a:b:c:d:v" OPTION +do + case $OPTION in + h) + usage + exit 1 + ;; + i) + iters=$OPTARG + ;; + 1) + b1=$OPTARG + ;; + 2) + b2=$OPTARG + ;; + a) + pa=$OPTARG + ;; + b) + pb=$OPTARG + ;; + c) + ca=$OPTARG + ;; + d) + cb=$OPTARG + ;; + s) + scriptdir=$OPTARG + ;; + v) + verbose=1 + ;; + ?) + usage + exit + ;; + esac +done + +if [ $b1 == "none" ]; then + echo "Binding 1 id is required. Exiting ..." + exit +fi + +if [ $b2 == "none" ]; then + echo "Binding 2 id is required. Exiting ..." + exit +fi + +if [ $pa == "none" ]; then + echo "Program a is required. Exiting ..." + exit +fi + +if [ $pb == "none" ]; then + echo "Program b is required. Exiting ..." + exit +fi + +pa=$scriptdir/$pa +pb=$scriptdir/$pb + +if [ $iters -eq 0 ]; then + iters=1 +fi + +bid1dir=/srv/bindings/$b1/files +bid2dir=/srv/bindings/$b2/files + +pass=0 +fail=0 + +if [ $verbose -eq 1 ]; then + starttime=$(date +%s) + echo "Program a on binding 1: $pa -b $bid1dir $ca" + echo "Program b on binding 2: $pb -b $bid2dir $cb" + echo "Script dir is $scriptdir" +fi + +iter=1 +while [ $iter -le $iters ] +do + if [ $verbose -eq 1 ]; then + echo "$iter: " + fi + $pa -b $bid1dir $ca + if [ $? -eq 0 ]; then + pass=$((pass + 1)) + else + fail=$((fail + 1)) + fi + $pb -b $bid2dir $cb + iter=$((iter + 1)) +done + +if [ $verbose -eq 1 ]; then + endtime=$(date +%s) + elapsedtime=$(( $endtime - $starttime )) + echo "Elapsed time: $elapsedtime" +fi + +if [ $fail -ne 0 ]; then + echo "FAIL: tests failed $fail; files passed $pass" +else + echo "PASS: tests passed $pass" +fi + +echo "EXIT dual-binding-tests" +exit $fail + diff --git a/tests/dual-binding-tests.sh b/tests/dual-binding-tests.sh new file mode 100755 index 00000000..d15e3947 --- /dev/null +++ b/tests/dual-binding-tests.sh @@ -0,0 +1,169 @@ +#! /bin/bash +set +e + +# This is a wrapper script. It calls dual-binding-test.sh for a variety of tests which can +# test how one binding should respond when a different binding sees a particular request. + +usage() +{ +cat << EOF +usage: $0 options + +This script runs a series of dual binding tests + +OPTIONS: + -h Show this message + -i Number of iterations + -d Test directory + -v Verbose +EOF +} + +iters=0 +verbose=0 +testdir=/root/pantheon/fusedav/tests/ +bid1=0 +bid2=0 + +while getopts "hi:d:1:2:v" OPTION +do + case $OPTION in + h) + usage + exit 1 + ;; + i) + iters=$OPTARG + ;; + d) + testdir=$OPTARG + ;; + 1) + bid1=$OPTARG + ;; + 2) + bid2=$OPTARG + ;; + v) + verbose=1 + ;; + ?) + usage + exit + ;; + esac +done + +if [ $iters -eq 0 ]; then + iters=1 +fi + +pass=0 +fail=0 + +if [ $verbose -eq 1 ]; then + starttime=$(date +%s) +fi + +# Just list the tests to be run +# basic ops are: +# list: ls -al +# listR: ls -alR +# makedir: mkdir +# writefile: head -c 67; also give it a number of iters to write many files +# readfile: cat +# removefiles: rm -f +# removedir: rmdir +# Each of the commands has an 'f' version, e.g. flist +# The 'f' version expects failure, so failure is counted as pass + +echo "ENTER dual-binding-tests" + +iter=1 +while [ $iter -le $iters ] +do + echo; echo "TEST 1"; echo + echo; echo "PRELIMINARY"; echo + + # Preliminary: remove files and dir to set a clean slate + $testdir/dual-binding-test.sh -1 $bid1 -2 $bid2 -a basic-ops.sh -b basic-ops.sh -c "-i 1 -c removefiles;removedir" -d "-c null" + + # On the first binding, make a directory, put some files in it, + # remove the files, then remove directory via the second binding + echo; echo "TEST"; echo + + # Run the real test + $testdir/dual-binding-test.sh -1 $bid1 -2 $bid2 -a basic-ops.sh -b basic-ops.sh -v -c "-v -i 1 -c sleep;makedir;writefile;removefiles" -d "-v -c removedir" + + ret=$? + if [ $ret -eq 0 ]; then + pass=$((pass + 1)) + else + fail=$((fail + 1)) + fi + +######################################################################### + echo; echo "TEST 2"; echo + echo; echo "PRELIMINARY"; echo + + # remove files and dir to set a clean slate + $testdir/dual-binding-test.sh -1 $bid1 -2 $bid2 -a basic-ops.sh -b basic-ops.sh -c "-i 1 -c removefiles;removedir" -d "-c null" + + # Add removefiles and removedir afterward, to clean up + echo; echo "TEST"; echo + + # On the first binding, make a directory, and put some files in it, then + # remove directory via the second binding, which should fail (use the 'f' version to pass on failure) + $testdir/dual-binding-test.sh -1 $bid1 -2 $bid2 -a basic-ops.sh -b basic-ops.sh -v -c "-v -i 1 -c sleep;makedir;writefile" -d "-v -c fremovedir;removefiles;removedir" + ret=$? + if [ $ret -eq 0 ]; then + pass=$((pass + 1)) + else + fail=$((fail + 1)) + fi + + iter=$((iter + 1)) + +######################################################################### + echo; echo "TEST 3"; echo + echo; echo "PRELIMINARY"; echo + + # remove files and dir to set a clean slate + $testdir/dual-binding-test.sh -1 $bid1 -2 $bid2 -a basic-ops.sh -b basic-ops.sh -c "-i 1 -c removefiles;removedir" -d "-c null" + + # Add removefiles and removedir afterward, to clean up + echo; echo "TEST"; echo + + # On the first binding, make a directory, and put some files in it, then + # ls the binding, then remove some files but leave others, then ls the binding + # and expect to get back only the files which weren't removed + $testdir/dual-binding-test.sh -1 $bid1 -2 $bid2 -a basic-ops.sh -b basic-ops.sh -v -c "-v -i 2 -c sleep;makedir;writefile" -d "-v -c list;removefile;list;removefiles;removedir" + echo "Check the two lists; one should be missing basic-ops-file-1" + ret=$? + if [ $ret -eq 0 ]; then + pass=$((pass + 1)) + else + fail=$((fail + 1)) + fi + + iter=$((iter + 1)) + +######################################################################### + + +done + +######################################################################### +if [ $verbose -eq 1 ]; then + endtime=$(date +%s) + elapsedtime=$(( $endtime - $starttime )) + echo "Elapsed time: $elapsedtime" +fi + +if [ $fail -ne 0 ]; then + echo "FAIL: tests failed $fail; files passed $pass" +else + echo "PASS: tests passed $pass" +fi + +echo "EXIT dual-binding-tests" diff --git a/tests/dual-binding-write.sh b/tests/dual-binding-write.sh new file mode 100755 index 00000000..fad61255 --- /dev/null +++ b/tests/dual-binding-write.sh @@ -0,0 +1,153 @@ +#! /bin/bash +set +e + +usage() +{ +cat << EOF +usage: $0 options + +This script tests that a file created on one binding is immediately available when accessed +on a different binding in the environment. + +OPTIONS: + -h Show this message + -i Number of iterations + -v Verbose + -1 First binding id + -2 Second binding id +EOF +} + +iters=16 +doubleiters=32 +verbose=0 +b1="none" +b2="none" + +echo "ENTER dual-binding-write" + +while getopts "hi:1:2:v" OPTION +do + case $OPTION in + h) + usage + exit 1 + ;; + i) + iters=$OPTARG + ;; + 1) + b1=$OPTARG + ;; + 2) + b2=$OPTARG + ;; + v) + verbose=1 + ;; + ?) + usage + exit + ;; + esac +done + +if [ $b1 == "none" ]; then + echo "Binding 1 id is required. Exiting ..." + exit +fi + +if [ $b2 == "none" ]; then + echo "Binding 2 id is required. Exiting ..." + exit +fi + +bid1path=/srv/bindings/$b1 +bid2path=/srv/bindings/$b2 +filedir=dual-binding-dir + +if [ $verbose -eq 1 ]; then + starttime=$(date +%s) +fi + +# We used to have a two-second window which if kept open, would trigger +# a 404 on one binding when a different binding had just created a file. +# Simulate that just to make sure we have solved the problem. + +repeated_ls(){ + while true; do + if [ $verbose -eq 1 ]; then + echo "ls $1" + fi + ls $1 + sleep 1 + done +} + +#### Set up +# Cleanup whatever might be in there +rm -f $bid1path/files/$filedir/* +rmdir $bid1path/files/$filedir + +# make the directory for the new file and put a seed file there +# (just to have a non-empty directory) +mkdir $bid1path/files/$filedir +echo "seedfile" > $bid1path/files/$filedir/seedfile.txt +# Keep accessing the directory on bid1 to trigger the +# 2-second propfind window which should no longer exist +repeated_ls $bid1path/files/$filedir & +# Save of the function pid to kill it later +lspid1=$! +repeated_ls $bid2path/files/$filedir & +# Save of the function pid to kill it later +lspid2=$! + +iter=0 +# Write several new files to bid2 and expect them to appear on bid1 without delay +while [ $iter -lt $doubleiters ]; do + inneriter=0 + while [ $inneriter -lt $iter ]; do + head -c 67 /dev/urandom > $bid2path/files/$filedir/file-$iter-$inneriter.txt + cat $bid1path/files/$filedir/file-$iter-$inneriter.txt > /dev/null 2>&1 + + if [ $? -eq 0 ]; then + pass=$((pass + 1)) + if [ $verbose -eq 1 ]; then + echo "iter $iter-$inneriter: pass" + fi + else + fail=$((fail + 1)) + if [ $verbose -eq 1 ]; then + echo "iter $iter-$inneriter: fail" + fi + fi + + # Sleep from 0 - iter seconds the first half, + # then just 1 second each time the second half + # Just testing that the sleep interval does not + # matter for the ability to pick up the new file + if [ $iter -lt $iters ]; then + sleep $iter + else + sleep 1 + fi + inneriter=$((inneriter + 1)) + done + iter=$((iter + 1)) +done + +kill $lspid1 +kill $lspid2 + +if [ $verbose -eq 1 ]; then + endtime=$(date +%s) + elapsedtime=$(( $endtime - $starttime )) + echo "Elapsed time: $elapsedtime" +fi + +if [ $fail -ne 0 ]; then + echo "FAIL: tests failed $fail; files passed $pass" +else + echo "PASS: tests passed $pass" +fi + diff --git a/tests/forensic-haven-cleanup.sh b/tests/forensic-haven-cleanup.sh index f7564e3b..887210a7 100755 --- a/tests/forensic-haven-cleanup.sh +++ b/tests/forensic-haven-cleanup.sh @@ -39,7 +39,7 @@ if [ -f ../fusedav.conf ]; then fi if [ ! -f fusedav.conf ]; then - echo "ERROR: Need to be in /srv/bindings/ directory" + echo "ERROR: Need to be in /srv/bindings/ directory: $(pwd)" exit fi diff --git a/tests/pfbackoff.sh b/tests/pfbackoff.sh new file mode 100755 index 00000000..8027e2c6 --- /dev/null +++ b/tests/pfbackoff.sh @@ -0,0 +1,249 @@ +#! /bin/bash +set +e + +usage() +{ +cat << EOF +usage: $0 options + +This script tests that non-existent files using a fibonacci backoff +properly suppress or allow a propfind. This first test accesses files in less time than +the fibonacci backoff value, so that no propfinds are generated. The second test +waits for the required fibonacci backoff to have passed, so that propfinds +are generated. + +This script calls journalctl to get log messages, and uses their content +to ascertain success or failure. The log entries required are those +which don't print under production configuration. So the binding they +are run on requires that verbose logging be enabled. The script will +modify fusedav.conf to contain the following values: + +log_level=6 +log_level_by_section=600070707000000000700007 + +OPTIONS: + -h Show this message + -v Verbose + -n Number of iterations. Default 5 makes test take about a minute. +EOF +} + +numattempts=6 +verbose=0 + +while getopts "hvn:" OPTION +do + case $OPTION in + h) + usage + exit 1 + ;; + n) + numattempts=$OPTARG + ;; + v) + verbose=1 + ;; + ?) + usage + exit 1 + ;; + esac +done + +# Most tests need to be in the files directory, but this one needs to be +# one up. +if [ -f ../fusedav.conf ]; then + cd .. +fi + +if [ ! -f fusedav.conf ]; then + echo "ERROR: Need to be in /srv/bindings/ directory: $(pwd)" + exit +fi + +# e.g. /srv/bindings/ +path=$(pwd) +# binding id +bid=${path##*/} +# output file for journalctl, from which are read results +outfile=/tmp/propfind-negative-entry-backoff.out +# name of non-existent file +missingfile=files/basic-ops/missing-file.file +# We could use ${##/} notation, but that would likely complicate it +basemissingfile=missing-file.file + +# The fibonacci sequence; if fewer than this many seconds have passed +# since last ENOENT propfind on the missing file, no propfind is generated +fibs=(0 1 2 3 5 8 13 21 34 55 89 144 233 377 610) + +if [ $verbose -eq 1 ]; then + starttime=$(date +%s) +fi + +numattempt=0 + +pass=0 +fail=0 + + +##### TEST 1 +# Access a missing file on propfind fibonacci boundaries. Expect that +# each access will cause a new propfind + +# For journalctl +since=$(date '+%Y-%m-%d %H:%M:%S') + +# Set fusedav.conf for logging needed to get results from journalctl to parse +mv /srv/bindings/$bid/fusedav.conf /srv/bindings/$bid/fusedav.conf.1 +sed 's/log_level=.*/log_level=6/' /srv/bindings/$bid/fusedav.conf.1 > /srv/bindings/$bid/fusedav.conf.2 +sed 's/log_level_by_section=.*/log_level_by_section=600070707000000000700007/' /srv/bindings/$bid/fusedav.conf.1 > /srv/bindings/$bid/fusedav.conf + +# Reset the state of the 'missing file' to first attempt. +if [ $verbose -eq 1 ]; then + btool invalidate srv-bindings-$bid-files.mount +else + btool invalidate srv-bindings-$bid-files.mount > /dev/null 2>&1 +fi + + +while [ $numattempt -lt $numattempts ] +do + if [ $verbose -eq 1 ]; then + echo "$numattempt: cat $missingfile" + date '+%Y-%m-%d %H:%M:%S' + fi + + sleeptime=${fibs[$numattempt]} + + if [ $verbose -eq 1 ]; then + echo "Sleeping $sleeptime seconds" + fi + sleep $sleeptime + + if [ $verbose -eq 1 ]; then + cat $missingfile + else + cat $missingfile > /dev/null 2>&1 + fi + + numattempt=$(( numattempt + 1 )) +done + +if [ $verbose -eq 1 ]; then + echo "journalctl -a -u srv-bindings-$bid-files.mount --since $since --no-pager > $outfile" +fi +journalctl -a -u srv-bindings-$bid-files.mount --since "$since" --no-pager > $outfile + +# If the log level is not high enough, we won't get any output. This is an error. +gjctl=$(grep "requires_propfind.*$basemissingfile" $outfile) +if [ $? -ne 0 ]; then + fail=$((fail + 1)) + echo "FAIL: TEST 1: gjctl" +else + pass=$((pass + 1)) + + if [ $verbose -eq 1 ]; then + echo "OUTPUT: $gjctl" + fi + + # If we see "no propfind needed", we have failed; + grep -q "no propfind needed" $outfile + + if [ $? -eq 0 ]; then + fail=$((fail + 1)) + echo "FAIL: TEST 1" + else + pass=$((pass + 1)) + echo "PASS: TEST 1" + fi +fi + +#### TEST 2 +# Access the missing file. Some should trigger propfind, others not, +# depending on where the access is in the fibonacci backoff +# Start with the state of the previous test. We should be in an +# 8-second window where propfinds won't be triggered + +# Create a gap for the new journalctl call +sleep 1 + +# For journalctl +since=$(date '+%Y-%m-%d %H:%M:%S') + +attempt=0 +attempts=3 +sleeptime=2 + +while [ $attempt -lt $attempts ] +do + if [ $verbose -eq 1 ]; then + echo "$attempt: cat $missingfile" + date '+%Y-%m-%d %H:%M:%S' + fi + + if [ $verbose -eq 1 ]; then + cat $missingfile + else + cat $missingfile > /dev/null 2>&1 + fi + + attempt=$(( attempt + 1 )) + + # Sleep 2 seconds. This should allow 3 attempts at the + # missing file while we are still in the fibonacci + # backoff window + if [ $verbose -eq 1 ]; then + echo "Sleeping $sleeptime seconds" + fi + sleep $sleeptime + +done + +echo "journalctl -a -u srv-bindings-$bid-files.mount --since $since --no-pager > $outfile" +journalctl -a -u srv-bindings-$bid-files.mount --since "$since" --no-pager > $outfile + +gjctl=$(grep "requires_propfind.*$basemissingfile" $outfile) +if [ $? -ne 0 ]; then + fail=$((fail + 1)) + echo "FAIL: TEST 2: gjctl" +else + pass=$((pass + 1)) + + if [ $verbose -eq 1 ]; then + echo "OUTPUT: $gjctl" + fi + + # If we see "new propfind for path", we have failed; + grep -q "new propfind for path" $outfile + + if [ $? -eq 0 ]; then + fail=$((fail + 1)) + echo "FAIL: TEST 2" + else + pass=$((pass + 1)) + echo "PASS: TEST 2" + fi +fi + +#### Clean Up +# Set fusedav.conf back to original and do a remount to invoke +mv /srv/bindings/$bid/fusedav.conf.1 /srv/bindings/$bid/fusedav.conf +rm -f /srv/bindings/$bid/fusedav.conf.2 +if [ $verbose -eq 1 ]; then + btool remount srv-bindings-$bid-files.mount +else + btool remount srv-bindings-$bid-files.mount > /dev/null 2>&1 +fi + +if [ $verbose -eq 1 ]; then + endtime=$(date +%s) + elapsedtime=$(( $endtime - $starttime )) + echo "Elapsed time: $elapsedtime" +fi + +if [ $fail -ne 0 ]; then + echo "FAIL: tests failed $fail; files passed $pass" +else + echo "PASS: tests passed $pass" +fi diff --git a/tests/rename.c b/tests/rename.c index c228a8c0..98948de7 100644 --- a/tests/rename.c +++ b/tests/rename.c @@ -84,7 +84,7 @@ int main(int argc, char *argv[]) { int opt; bool fail = false; - while ((opt = getopt (argc, argv, "uvhf:")) != -1) { + while ((opt = getopt (argc, argv, "uvh")) != -1) { switch (opt) { case 'v': diff --git a/tests/urlencode.sh b/tests/urlencode.sh index b0b9435f..535ce924 100755 --- a/tests/urlencode.sh +++ b/tests/urlencode.sh @@ -6,7 +6,9 @@ usage() cat << EOF usage: $0 options -This script runs the command line tests +This script runs the command line tests, but it does so by adding +special characters to the filename and ensuring that they are properly +processed by the backend OPTIONS: -h Show this message diff --git a/tests/write-delete-checknegative.sh b/tests/write-delete-checknegative.sh new file mode 100755 index 00000000..5030f0fe --- /dev/null +++ b/tests/write-delete-checknegative.sh @@ -0,0 +1,215 @@ +#! /bin/bash +set +e + +usage() +{ +cat << EOF +usage: $0 options + +This script tests that files written and deleted create a negative entry in +the stat cache and those written and not deleted don't. + +This script gets the pid of the binding, sends a SIGUSR2 signal which dumps +the stat cache, then calls journalctl to get log messages, and uses their content +to ascertain success or failure. + +OPTIONS: + -h Show this message + -v Verbose + -n Number of iterations. +EOF +} + +iters=16 +verbose=0 + +while getopts "hvn:" OPTION +do + case $OPTION in + h) + usage + exit 1 + ;; + n) + iters=$OPTARG + ;; + v) + verbose=1 + ;; + ?) + usage + exit 1 + ;; + esac +done + +if [ -f ../fusedav.conf ]; then + cd .. +fi + +if [ ! -f fusedav.conf ]; then + echo "ERROR: Need to be in /srv/bindings/ directory: $(pwd)" + exit +fi + +# e.g. /srv/bindings/ +path=$(pwd) +# binding id +bid=${path##*/} +# base name +bn=write-delete-checknegative +# base dir +bd=files/$bn-dir +# base file +bf=$bd/$bn +# output file for journalctl, from which are read results +outfile=/tmp/$bn.out + +if [ $verbose -eq 1 ]; then + starttime=$(date +%s) +fi + +iter=0 + +pass=0 +fail=0 + +##### TEST 1 +# Create a number of iles + +if [ $verbose -eq 1 ]; then + echo "mkdir $bd" +fi + +rm -f $bd/* +rmdir $bd + +mkdir $bd > /dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "Failed to make directory $bd. Exiting..." + exit +fi + +# For journalctl +since=$(date '+%Y-%m-%d %H:%M:%S') + +# Reset the state of the files for the first attempts. +# If this test has already been run and the files still +# exist in the stat cache, the results are invalid +if [ $verbose -eq 1 ]; then + btool invalidate srv-bindings-$bid-files.mount +else + btool invalidate srv-bindings-$bid-files.mount > /dev/null 2>&1 +fi + +# PID of fusedav mount; needed later to send it a signal to dump stat cache +fdpid=$(ps -A -o pid,cmd | grep fusedav | grep $bid | grep fusedav.conf | awk '{print $1}') +if [ $verbose -eq 1 ]; then + echo "PID: $fdpid" +fi + +# Create the files ... +while [ $iter -lt $iters ] +do + if [ $verbose -eq 1 ]; then + echo "$iter: echo $bf-$iter.file > $bf-$iter.file" + fi + + echo "$bf-$iter.file" > $bf-$iter.file + + iter=$(( iter + 1 )) +done + +# Delete some files +iter=0 +while [ $iter -lt $iters ] +do + # Delete all files on iter % 3; do not delete the others + df=$(( iter % 3 )) + if [ $df -eq 0 ]; then + if [ $verbose -eq 1 ]; then + echo "$iter: rm -f $bf-$iter.file" + fi + rm -f $bf-$iter.file + fi + + iter=$(( iter + 1 )) +done + +# Send signal to dump stat cache to journalctl +kill -SIGUSR2 $fdpid + +# Sleep some time to make sure the dump completes +sleep 8 + +if [ $verbose -eq 1 ]; then + echo "journalctl -a -u srv-bindings-$bid-files.mount --since $since --no-pager > $outfile" +fi +journalctl -a -u srv-bindings-$bid-files.mount --since "$since" --no-pager > $outfile + +# Check for positive or negative for files +iter=0 +while [ $iter -lt $iters ] +do + if [ $verbose -eq 1 ]; then + echo "$iter: grep *stat_cache_walk:.*$bn-$iter.file.*positive" $outfile + fi + pn=$(grep "stat_cache_walk:.*$bn-$iter.file.*positive" $outfile) + if [ $? -eq 0 ]; then + pos=1 + else + if [ $verbose -eq 1 ]; then + echo "$iter: grep *stat_cache_walk:.*$bn-$iter.file.*negative" $outfile + fi + pn=$(grep "stat_cache_walk:.*$bn-$iter.file.*negative" $outfile) + if [ $? -eq 0 ]; then + neg=1 + fi + fi + if [ $verbose -eq 1 ]; then + echo "$iter: grep on $bn-$iter.file: $pn" + fi + df=$(( iter % 3 )) + if [ $df -eq 0 ]; then + # Should have been deleted, so we should not expect 'positive' in the grep + if [ $neg ]; then + pass=$((pass + 1)) + if [ $verbose -eq 1 ]; then + echo "Passed on $bn-$iter.file: did not get positive" + fi + else + fail=$((fail + 1)) + if [ $verbose -eq 1 ]; then + echo "Failed on $bn-$iter.file: expected negative, got positive" + fi + fi + else + # Should not have been deleted, so we should expect 'positive' in the grep + if [ $pos ]; then + pass=$((pass + 1)) + if [ $verbose -eq 1 ]; then + echo "Passed on $bn-$iter.file: got positive" + fi + else + fail=$((fail + 1)) + if [ $verbose -eq 1 ]; then + echo "Failed on $bn-$iter.file: expected positive but did not get it" + fi + fi + fi + + iter=$(( iter + 1 )) +done + +#### Clean Up +if [ $verbose -eq 1 ]; then + endtime=$(date +%s) + elapsedtime=$(( $endtime - $starttime )) + echo "Elapsed time: $elapsedtime" +fi + +if [ $fail -ne 0 ]; then + echo "FAIL: files failed $fail; files passed $pass" +else + echo "PASS: files passed $pass" +fi diff --git a/tests/writefiles.sh b/tests/writefiles-create.sh similarity index 83% rename from tests/writefiles.sh rename to tests/writefiles-create.sh index bdc4a1f1..e7a6b685 100755 --- a/tests/writefiles.sh +++ b/tests/writefiles-create.sh @@ -6,7 +6,7 @@ usage() cat << EOF usage: $0 options -This script runs the command line tests +This script creates a lot of files. OPTIONS: -h Show this message @@ -58,18 +58,28 @@ do esac done +# If no bid was passed in, see if we are in a directory where +# we can extract a valid bid if [ $bid == "none" ]; then - echo "-b is required. Exiting ..." - exit + if [ -f ../fusedav.conf ]; then + cd .. + fi + if [ ! -f fusedav.conf ]; then + echo "-b is required. Exiting ..." + exit + fi + curdir=$(pwd) + bid=${curdir##*/} fi + biddir=/srv/bindings/$bid/files if [ $numdirs -eq 0 ]; then - numdirs=100 + numdirs=10 fi if [ $numfiles -eq 0 ]; then - numfiles=1000 + numfiles=100 fi if [ $verbose -eq 1 ]; then diff --git a/tests/writefiles-remove.sh b/tests/writefiles-remove.sh new file mode 100755 index 00000000..e2a6fc46 --- /dev/null +++ b/tests/writefiles-remove.sh @@ -0,0 +1,120 @@ +#! /bin/bash +set +e + +usage() +{ +cat << EOF +usage: $0 options + +This script removes files created with the writefiles-create script + +OPTIONS: + -h Show this message + -b Binding uuid (required) + -f Number of files + -d Number of dirs + -s Start file number + -t Start dir number + -v Verbose +EOF +} + +numfiles=0 +numdirs=0 +verbose=0 +startnum=0 +startdir=0 +bid="none" + +while getopts "hb:d:f:s:t:v" OPTION +do + case $OPTION in + h) + usage + exit 1 + ;; + b) + bid=$OPTARG + ;; + f) + numfiles=$OPTARG + ;; + d) + numdirs=$OPTARG + ;; + s) + startnum=$OPTARG + ;; + t) + startdir=$OPTARG + ;; + v) + verbose=1 + ;; + ?) + usage + exit + ;; + esac +done + +# If no bid was passed in, see if we are in a directory where +# we can extract a valid bid +if [ $bid == "none" ]; then + if [ -f ../fusedav.conf ]; then + cd .. + fi + if [ ! -f fusedav.conf ]; then + echo "-b is required. Exiting ..." + exit + fi + curdir=$(pwd) + bid=${curdir##*/} +fi + +biddir=/srv/bindings/$bid/files + +if [ $numdirs -eq 0 ]; then + numdirs=10 +fi + +if [ $numfiles -eq 0 ]; then + numfiles=100 +fi + +if [ $verbose -eq 1 ]; then + starttime=$(date +%s) +fi + +iters=0 +numdir=1 +while [ $numdir -le $numdirs ] +do + dirnum=$(( $numdir + $startdir )) + dirname=$biddir/dir-$dirnum + + numfile=1 + while [ $numfile -le $numfiles ] + do + filenum=$(( $numfile + $startnum )) + filename=$dirname/file-$filenum + if [ $verbose -eq 1 ]; then + echo "$iters: rm -f $filename" + fi + rm -f $filename + numfile=$(( numfile + 1 )) + iters=$(( iters + 1 )) + done + + if [ $verbose -eq 1 ]; then + echo "$numdir: rmdir $dirname" + fi + rmdir $dirname + numdir=$(( numdir + 1 )) +done + +if [ $verbose -eq 1 ]; then + endtime=$(date +%s) + elapsedtime=$(( $endtime - $starttime )) + echo "Elapsed time: $elapsedtime" +fi