void
ngx_buffer_cache_release(
	ngx_buffer_cache_t* cache,
	u_char* key,
	uint32_t token)
{
	ngx_buffer_cache_entry_t* entry;
	ngx_buffer_cache_sh_t *sh = cache->sh;
	uint32_t hash;

	hash = ngx_crc32_short(key, BUFFER_CACHE_KEY_SIZE);

	ngx_shmtx_lock(&cache->shpool->mutex);

	if (!sh->reset)
	{
		entry = ngx_buffer_cache_rbtree_lookup(&sh->rbtree, key, hash);
		if (entry != NULL && entry->state == CES_READY && (uint32_t)entry->write_time == token)
		{
			(void)ngx_atomic_fetch_add(&entry->ref_count, -1);
		}
	}

	ngx_shmtx_unlock(&cache->shpool->mutex);
}
ngx_flag_t
ngx_buffer_cache_fetch(
	ngx_buffer_cache_t* cache,
	u_char* key,
	ngx_str_t* buffer,
	uint32_t* token)
{
	ngx_buffer_cache_entry_t* entry;
	ngx_buffer_cache_sh_t *sh = cache->sh;
	ngx_flag_t result = 0;
	uint32_t hash;

	hash = ngx_crc32_short(key, BUFFER_CACHE_KEY_SIZE);

	ngx_shmtx_lock(&cache->shpool->mutex);

	if (!sh->reset)
	{
		entry = ngx_buffer_cache_rbtree_lookup(&sh->rbtree, key, hash);
		if (entry != NULL && entry->state == CES_READY && 
			(cache->expiration == 0 || ngx_time() < (time_t)(entry->write_time + cache->expiration)))
		{
			result = 1;

			// update stats
			sh->stats.fetch_hit++;
			sh->stats.fetch_bytes += entry->buffer_size;

			// copy buffer pointer and size
			buffer->data = entry->start_offset;
			buffer->len = entry->buffer_size;
			*token = entry->write_time;

			// Note: setting the access time of the entry and cache to prevent it 
			//		from being freed while the caller uses the buffer
			sh->access_time = entry->access_time = ngx_time();
			(void)ngx_atomic_fetch_add(&entry->ref_count, 1);
		}
		else
		{
			// update stats
			sh->stats.fetch_miss++;
		}
	}

	ngx_shmtx_unlock(&cache->shpool->mutex);

	return result;
}
ngx_flag_t
ngx_buffer_cache_store_gather(
	ngx_buffer_cache_t* cache, 
	u_char* key, 
	ngx_str_t* buffers,
	size_t buffer_count)
{
	ngx_buffer_cache_entry_t* entry;
	ngx_buffer_cache_sh_t *sh = cache->sh;
	ngx_str_t* cur_buffer;
	ngx_str_t* last_buffer;
	size_t buffer_size;
	uint32_t hash;
	uint32_t evictions;
	u_char* target_buffer;

	hash = ngx_crc32_short(key, BUFFER_CACHE_KEY_SIZE);

	ngx_shmtx_lock(&cache->shpool->mutex);

	if (sh->reset)
	{
		// a previous store operation was killed in progress, need to reset the cache
		// since the data structures may be corrupt. we can only reset the cache after
		// the access time expires since other processes may still be reading from / 
		// writing to the cache
		if (ngx_time() < sh->access_time + CACHE_LOCK_EXPIRATION)
		{
			ngx_shmtx_unlock(&cache->shpool->mutex);
			return 0;
		}

		// reset the cache, leave the reset flag enabled
		ngx_buffer_cache_reset(sh);

		// update stats
		sh->stats.reset++;
	}
	else
	{
		// remove expired entries
		if (cache->expiration)
		{
			for (evictions = MAX_EVICTIONS_PER_STORE; evictions > 0; evictions--)
			{
				if (!ngx_buffer_cache_free_oldest_entry(sh, cache->expiration))
				{
					break;
				}
			}
		}

		// make sure the entry does not already exist
		entry = ngx_buffer_cache_rbtree_lookup(&sh->rbtree, key, hash);
		if (entry != NULL)
		{
			sh->stats.store_exists++;
			ngx_shmtx_unlock(&cache->shpool->mutex);
			return 0;
		}

		// enable the reset flag before we start making any changes
		sh->reset = 1;
	}

	// allocate a new entry
	entry = ngx_buffer_cache_get_free_entry(sh);
	if (entry == NULL)
	{
		goto error;
	}

	// calculate the buffer size
	last_buffer = buffers + buffer_count;
	buffer_size = 0;
	for (cur_buffer = buffers; cur_buffer < last_buffer; cur_buffer++)
	{
		buffer_size += cur_buffer->len;
	}

	// allocate a buffer to hold the data
	target_buffer = ngx_buffer_cache_get_free_buffer(sh, buffer_size);
	if (target_buffer == NULL)
	{
		goto error;
	}

	// initialize the entry
	entry->state = CES_ALLOCATED;
	entry->node.key = hash;
	memcpy(entry->key, key, BUFFER_CACHE_KEY_SIZE);
	entry->start_offset = target_buffer;
	entry->buffer_size = buffer_size;

	// update the write position
	sh->buffers_write = target_buffer;

	// move from free_queue to used_queue
	ngx_queue_remove(&entry->queue_node);
	ngx_queue_insert_tail(&sh->used_queue, &entry->queue_node);

	// insert to rbtree
	ngx_rbtree_insert(&sh->rbtree, &entry->node);

	// update stats
	sh->stats.store_ok++;
	sh->stats.store_bytes += buffer_size;

	// Note: the memcpy is performed after releasing the lock to avoid holding the lock for a long time
	//		setting the access time of the entry and cache prevents it from being freed
	sh->access_time = entry->access_time = ngx_time();
	entry->write_time = ngx_time();

	sh->reset = 0;
	ngx_shmtx_unlock(&cache->shpool->mutex);

	for (cur_buffer = buffers; cur_buffer < last_buffer; cur_buffer++)
	{
		target_buffer = ngx_copy(target_buffer, cur_buffer->data, cur_buffer->len);
	}

	// Note: no need to obtain the lock since state is ngx_atomic_t
	entry->state = CES_READY;

	return 1;

error:
	sh->stats.store_err++;
	sh->reset = 0;
	ngx_shmtx_unlock(&cache->shpool->mutex);
	return 0;
}