liaoxin01 commented on code in PR #68991:
URL: https://github.com/apache/doris/pull/68991#discussion_r4313555226
##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -2106,25 +2172,225 @@ std::string
BlockFileCache::dump_single_cache_type_unlocked(const UInt128Wrapper
return cache_type_to_string(cell->second.file_block->cache_type());
}
-void BlockFileCache::change_cache_type(const UInt128Wrapper& hash, size_t
offset,
- FileCacheType new_type,
- std::lock_guard<std::mutex>&
cache_lock) {
- if (auto iter = _files.find(hash); iter != _files.end()) {
- auto& file_blocks = iter->second;
- if (auto cell_it = file_blocks.find(offset); cell_it !=
file_blocks.end()) {
- FileBlockCell& cell = cell_it->second;
- auto& cur_queue = get_queue(cell.file_block->cache_type());
- DCHECK(cell.queue_iterator.has_value());
- cur_queue.remove(*cell.queue_iterator, cache_lock);
- _lru_recorder->record_queue_event(
- cell.file_block->cache_type(), CacheLRULogType::REMOVE,
- cell.file_block->get_hash_value(),
cell.file_block->offset(), cell.size());
- auto& new_queue = get_queue(new_type);
- cell.queue_iterator =
- new_queue.add(hash, offset,
cell.file_block->range().size(), cache_lock);
- _lru_recorder->record_queue_event(new_type, CacheLRULogType::ADD,
-
cell.file_block->get_hash_value(),
- cell.file_block->offset(),
cell.size());
+bool BlockFileCache::is_protected(const FileBlock& block, int64_t now) {
+ return is_protected(block.tablet_id(),
static_cast<int64_t>(block.expiration_time()), now);
+}
+
+bool BlockFileCache::is_protected(int64_t tablet_id, int64_t legacy_deadline,
int64_t now) {
+ bool legacy = false;
+ const int64_t deadline = _policy_registry->protected_until(tablet_id,
legacy_deadline, &legacy);
+ if (deadline <= now) {
+ return false;
+ }
+ if (legacy) {
+ int64_t next = _next_legacy_deadline.load(std::memory_order_relaxed);
+ while (deadline < next && !_next_legacy_deadline.compare_exchange_weak(
+ next, deadline,
std::memory_order_relaxed)) {
+ }
+ }
+ return true;
+}
+
+FileCacheType BlockFileCache::queue_type_for(int64_t tablet_id, int64_t
legacy_deadline,
+ FileCacheType cache_class,
int64_t now) {
+ DCHECK(cache_class != FileCacheType::TTL);
+ return is_protected(tablet_id, legacy_deadline, now) ? FileCacheType::TTL
: cache_class;
+}
+
+void BlockFileCache::move_cell_to_queue(FileBlockCell& cell, FileCacheType
new_type,
+ std::lock_guard<std::mutex>&
cache_lock) {
+ auto& file_block = *cell.file_block;
+ const FileCacheType old_type = file_block.cache_type();
+ DCHECK(old_type != new_type);
+ DCHECK(cell.queue_iterator.has_value());
+ get_queue(old_type).remove(*cell.queue_iterator, cache_lock);
+ _lru_recorder->record_queue_event(old_type, CacheLRULogType::REMOVE,
+ file_block.get_hash_value(),
file_block.offset(),
+ cell.size());
+ cell.queue_iterator = get_queue(new_type).add_front(
+ file_block.get_hash_value(), file_block.offset(), cell.size(),
cache_lock);
+ _lru_recorder->record_queue_event(new_type, CacheLRULogType::ADDTOFRONT,
+ file_block.get_hash_value(),
file_block.offset(),
+ cell.size());
+ file_block._key.meta.type = new_type;
+ *_ttl_relocated_blocks << 1;
+}
+
+void BlockFileCache::add_cached_tablet(int64_t tablet_id) {
+ if (tablet_id > 0) {
+ ++_cached_tablets[tablet_id];
+ }
+}
+
+void BlockFileCache::remove_cached_tablet(int64_t tablet_id) {
+ if (tablet_id > 0) {
+ auto it = _cached_tablets.find(tablet_id);
+ DCHECK(it != _cached_tablets.end());
+ if (--it->second == 0) {
+ _cached_tablets.erase(it);
+ }
+ }
+}
+
+void BlockFileCache::relocate_to_ttl_queue(const std::vector<FileBlockCell*>&
cells,
+ std::lock_guard<std::mutex>&
cache_lock) {
+ for (auto* cell : cells) {
+ move_cell_to_queue(*cell, FileCacheType::TTL, cache_lock);
+ }
+}
+
+void BlockFileCache::complete_loaded_cell(FileBlockCell& cell, int64_t
tablet_id,
+ FileCacheType cache_class, uint64_t
legacy_deadline,
+ std::lock_guard<std::mutex>&
cache_lock) {
+ auto& meta = cell.file_block->_key.meta;
+ remove_cached_tablet(meta.tablet_id);
+ add_cached_tablet(tablet_id);
+ meta.tablet_id = tablet_id;
+ meta.cache_class = cache_class_of(cache_class);
+ meta.expiration_time = legacy_deadline;
+ const FileCacheType queue_type = queue_type_for(
+ tablet_id, static_cast<int64_t>(legacy_deadline),
meta.cache_class, UnixSeconds());
+ if (queue_type != meta.type) {
+ move_cell_to_queue(cell, queue_type, cache_lock);
+ }
+}
+
+void BlockFileCache::start_queue_sweeps(int64_t now) {
+ auto& state = _queue_sweep;
+ auto is_active = [&](FileCacheType type) { return
state.active[file_cache_type_index(type)]; };
+ const bool backstop_due =
+ now - state.last_backstop_at >=
config::file_cache_queue_sweep_backstop_interval_s;
+ // A pass over the TTL queue picks up blocks whose tablet lost its TTL, or
whose deadline
+ // passed, since the previous pass started. A pass under way is never
restarted; what
+ // changes meanwhile starts the next one.
+ if (!is_active(FileCacheType::TTL)) {
+ const uint64_t lost_version =
_policy_registry->protection_lost_version();
+ if (backstop_due || lost_version != state.ttl_pass_lost_version ||
+ _policy_registry->has_deadline_in(state.ttl_pass_started_at, now)
||
+ _next_legacy_deadline.load(std::memory_order_relaxed) <= now) {
+ state.active[file_cache_type_index(FileCacheType::TTL)] = true;
+ // What the next pass is compared with.
+ state.ttl_pass_lost_version = lost_version;
+ state.ttl_pass_started_at = now;
+ state.ttl_pass_legacy_bytes = 0;
+ // The pass finds again those still ahead.
+ _next_legacy_deadline.store(std::numeric_limits<int64_t>::max(),
+ std::memory_order_relaxed);
+ }
+ }
+ // A pass over the class queues picks up blocks whose tablet gained a TTL.
The three queues
+ // start together so that the version they record holds for all of them.
+ constexpr std::array<FileCacheType, 3> class_types {FileCacheType::INDEX,
FileCacheType::NORMAL,
+
FileCacheType::DISPOSABLE};
+ if (std::none_of(class_types.begin(), class_types.end(), is_active)) {
+ const uint64_t gained_version =
_policy_registry->protection_gained_version();
+ if (backstop_due || gained_version != state.class_pass_gained_version)
{
+ for (auto type : class_types) {
+ state.active[file_cache_type_index(type)] = true;
+ }
+ state.class_pass_gained_version = gained_version;
+ }
+ }
+ // The backstop counts as done once every queue has a pass; a queue still
busy with an older
+ // pass gets its own when that one ends.
+ if (backstop_due &&
+ std::all_of(state.active.begin(), state.active.end(), [](bool active)
{ return active; })) {
+ state.last_backstop_at = now;
+ }
+}
+
+size_t BlockFileCache::sweep_queue(FileCacheType type, size_t budget, int64_t
now,
+ std::lock_guard<std::mutex>& cache_lock) {
+ auto& active = _queue_sweep.active[file_cache_type_index(type)];
+ if (!active) {
+ return 0;
+ }
+ auto& queue = get_queue(type);
+ auto& pass_left = _queue_sweep.pass_left[file_cache_type_index(type)];
+ if (!queue.sweep_cursor.has_value()) {
+ // A pass walks at most as many blocks as the queue holds when it
starts: blocks touched
+ // or added meanwhile go to the hot end, and must not keep the pass
from ending. A block
+ // touched before the cursor reached it may be left for the next pass.
+ queue.sweep_cursor = queue.begin();
+ pass_left = queue.queue.size();
+ }
+ // The cursor lives on the queue, so the pass resumes there after the
cache lock was released.
+ auto& cursor = *queue.sweep_cursor;
+ size_t walked = 0;
+ std::vector<std::pair<FileBlockCell*, FileCacheType>> moves;
+ for (; walked < budget && pass_left > 0 && cursor != queue.end();
++walked, --pass_left) {
+ auto& entry = *cursor;
+ // Past the entry before it may move.
+ ++cursor;
+ auto* cell = get_cell(entry.hash, entry.offset, cache_lock);
+ DCHECK(cell != nullptr);
+ const auto& block = *cell->file_block;
+ // Where the block belongs now: the TTL queue while protected, else
its class queue.
+ const FileCacheType want =
+ is_protected(block, now) ? FileCacheType::TTL :
block.cache_class();
+ if (want != type) {
+ moves.emplace_back(cell, want);
+ } else if (type == FileCacheType::TTL && block.tablet_id() <= 0) {
+ _queue_sweep.ttl_pass_legacy_bytes += cell->size();
+ }
+ }
+ // Moved after the walk, all of them behind the cursor.
+ for (auto& [cell, want] : moves) {
+ move_cell_to_queue(*cell, want, cache_lock);
+ }
+ // The pass is over; start_queue_sweeps() decides whether another one is
due.
+ if (pass_left == 0 || queue.sweep_cursor == queue.end()) {
+ queue.sweep_cursor.reset();
+ active = false;
+ if (type == FileCacheType::TTL) {
+
_ttl_legacy_v2_protected_bytes->set_value(_queue_sweep.ttl_pass_legacy_bytes);
+ }
+ }
+ return walked;
+}
+
+// How often a cache path tells the policy registry which tablets it holds
blocks of.
+constexpr int64_t kReportCachedTabletsIntervalS = 60;
+
+void BlockFileCache::run_background_queue_sweep() {
+ Thread::set_self_name("run_background_queue_sweep");
+ while (!_close) {
+ {
+ std::unique_lock close_lock(_close_mtx);
+ _close_cv.wait_for(close_lock, std::chrono::milliseconds(
+
config::file_cache_queue_sweep_interval_ms));
+ if (_close) {
+ break;
+ }
+ }
+ // Blocks being loaded are put in the right queue as they are loaded.
+ if (!_async_open_done) {
Review Comment:
Fixed in 82f233f9f0a: the startup reservation now skips protected blocks and
moves them to the TTL queue, like eviction after loading, within the same skip
and relocation bounds.
##########
be/src/io/cache/tablet_cache_policy_registry.cpp:
##########
@@ -0,0 +1,448 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "io/cache/tablet_cache_policy_registry.h"
+
+#include <bvar/bvar.h>
+
+#include <algorithm>
+#include <chrono>
+#include <limits>
+
+#include "common/config.h"
+#include "common/logging.h"
+#include "cpp/sync_point.h"
+#include "io/cache/cache_block_meta_store.h"
+#include "runtime/exec_env.h"
+#include "storage/tablet/tablet_meta.h"
+#include "util/thread.h"
+#include "util/time.h"
+
+namespace doris::io {
+
+bvar::Adder<uint64_t>
g_tablet_cache_policy_fetch_num("file_cache_tablet_policy_fetch_num");
+bvar::Adder<uint64_t> g_tablet_cache_policy_fetch_failed_num(
+ "file_cache_tablet_policy_fetch_failed_num");
+bvar::PassiveStatus<size_t> g_tablet_cache_policy_num(
+ "file_cache_tablet_policy_num",
+ [](void*) { return TabletCachePolicyRegistry::instance()->size(); },
nullptr);
+
+namespace {
+
+// How often the record is scanned for policies nobody needs any more.
+constexpr int64_t kPruneIntervalS = 60;
+// How long a tablet whose policy could not be fetched is left alone.
+constexpr int64_t kFetchRetryIntervalS = 60;
+// How many reported tablets are looked up per hold of the exclusive lock.
+constexpr size_t kReportBatchSize = 1024;
+// How often the background refresh runs.
+constexpr std::chrono::seconds kRefreshInterval {1};
+
+Status fetch_from_tablet_meta(int64_t tablet_id, TabletCachePolicy* policy) {
+ TabletMetaSharedPtr tablet_meta;
+ RETURN_IF_ERROR(ExecEnv::get_tablet_meta(tablet_id, &tablet_meta, false));
+ DORIS_CHECK(tablet_meta != nullptr);
+ policy->ttl_seconds = tablet_meta->ttl_seconds();
+ policy->ctime = tablet_meta->creation_time();
+ return Status::OK();
+}
+
+} // namespace
+
+int64_t TabletCachePolicy::deadline() const {
+ if (ttl_seconds <= 0 || ctime <= 0) {
+ return 0;
+ }
+ // Same rule as TabletMeta::file_cache_ttl_expiration_time(): a huge ttl
must not wrap into
+ // a deadline in the past.
+ if (ctime > std::numeric_limits<int64_t>::max() - ttl_seconds) {
+ return std::numeric_limits<int64_t>::max();
+ }
+ return ctime + ttl_seconds;
+}
+
+TabletCachePolicyRegistry* TabletCachePolicyRegistry::instance() {
+ // Never destroyed: a cache path may still be running when the process
exits.
+ static auto* registry = new TabletCachePolicyRegistry();
+ return registry;
+}
+
+std::optional<int64_t> TabletCachePolicyRegistry::deadline(int64_t tablet_id)
const {
+ std::shared_lock lock(_mutex);
+ auto it = _entries.find(tablet_id);
+ if (it == _entries.end()) {
+ return std::nullopt;
+ }
+ return it->second.policy.deadline();
+}
+
+int64_t TabletCachePolicyRegistry::protected_until(int64_t tablet_id, int64_t
legacy_deadline,
+ bool* legacy) {
+ // tablet_id 0 is data outside any Doris tablet (external tables, v2 cache
files that never
+ // recorded one): it only has the deadline it was written with.
+ if (tablet_id > 0) {
+ if (const std::optional<int64_t> known = deadline(tablet_id);
known.has_value()) {
+ *legacy = false;
+ return *known;
+ }
+ note_tablet(tablet_id);
+ }
+ *legacy = true;
+ return legacy_deadline;
+}
+
+bool TabletCachePolicyRegistry::is_protected(int64_t tablet_id, int64_t
legacy_deadline,
+ int64_t now) {
+ bool legacy = false;
+ return protected_until(tablet_id, legacy_deadline, &legacy) > now;
+}
+
+std::optional<TabletCachePolicy> TabletCachePolicyRegistry::set_locked(
+ int64_t tablet_id, const TabletCachePolicy& policy, int64_t
refreshed_at_s) {
+ auto [it, inserted] =
+ _entries.try_emplace(tablet_id, Entry {policy, refreshed_at_s,
refreshed_at_s});
+ std::optional<TabletCachePolicy> before;
+ if (!inserted) {
+ before = it->second.policy;
+ if (const int64_t old_deadline = before->deadline(); old_deadline > 0)
{
+ _deadlines.erase(_deadlines.find(old_deadline));
+ }
+ it->second.policy = policy;
+ it->second.refreshed_at_s = refreshed_at_s;
+ }
+ if (const int64_t new_deadline = policy.deadline(); new_deadline > 0) {
+ _deadlines.insert(new_deadline);
+ }
+ return before;
+}
+
+std::optional<TabletCachePolicy>
TabletCachePolicyRegistry::erase_locked(int64_t tablet_id) {
+ auto it = _entries.find(tablet_id);
+ if (it == _entries.end()) {
+ return std::nullopt;
+ }
+ const TabletCachePolicy before = it->second.policy;
+ if (const int64_t old_deadline = before.deadline(); old_deadline > 0) {
+ _deadlines.erase(_deadlines.find(old_deadline));
+ }
+ _entries.erase(it);
+ return before;
+}
+
+void TabletCachePolicyRegistry::bump_versions(const
std::optional<TabletCachePolicy>& before,
+ const
std::optional<TabletCachePolicy>& after) {
+ auto bump = [](std::atomic<uint64_t>& version) {
+ version.fetch_add(1, std::memory_order_acq_rel);
+ };
+ // An unknown tablet is judged by the deadline each block carries, which
may be earlier or
+ // later, so learning or forgetting a tablet can move its blocks either
way.
+ if (!before.has_value() || !after.has_value()) {
+ bump(_protection_gained_version);
+ bump(_protection_lost_version);
+ return;
+ }
+ // A block is protected while the deadline is ahead, so only a change of
that at this moment
+ // moves blocks now: a TTL rewritten from one deadline ahead to another
moves none, and the
+ // new deadline starts a pass over the TTL queue when it passes
(has_deadline_in()).
+ const int64_t now = UnixSeconds();
+ const int64_t old_deadline = before->deadline();
+ const int64_t new_deadline = after->deadline();
+ if (old_deadline <= now && new_deadline > now) {
+ bump(_protection_gained_version);
+ }
+ // Also when both have passed: the pass the old deadline was to start may
not have run yet.
+ if (new_deadline < old_deadline && new_deadline <= now) {
+ bump(_protection_lost_version);
+ }
+}
+
+void TabletCachePolicyRegistry::persist(int64_t tablet_id,
+ const
std::optional<TabletCachePolicy>& policy) {
+ std::lock_guard lock(_stores_mutex);
+ for (auto* store : _stores) {
+ if (policy.has_value()) {
+ store->put_tablet_policy(tablet_id, *policy);
+ } else {
+ store->delete_tablet_policy(tablet_id);
+ }
+ }
+}
+
+void TabletCachePolicyRegistry::upsert(int64_t tablet_id, const
TabletCachePolicy& policy) {
+ DORIS_CHECK(tablet_id > 0);
+ const int64_t now = UnixSeconds();
+ {
+ // Unchanged and recently confirmed is the common case: every meta
refresh reports the
+ // policy. Only take the exclusive lock to record a change, or to push
the refresh time
+ // forward once it is half way to being due.
+ std::shared_lock lock(_mutex);
+ auto it = _entries.find(tablet_id);
+ if (it != _entries.end() && it->second.policy == policy &&
+ it->second.refreshed_at_s >
+ now - config::file_cache_tablet_policy_refresh_interval_s
/ 2) {
+ return;
+ }
+ }
+ std::optional<TabletCachePolicy> before;
+ {
+ std::unique_lock lock(_mutex);
+ before = set_locked(tablet_id, policy, now);
+ }
+ if (before == policy) {
+ return;
+ }
+ bump_versions(before, policy);
+ persist(tablet_id, policy);
+}
+
+void TabletCachePolicyRegistry::erase(int64_t tablet_id) {
+ std::optional<TabletCachePolicy> before;
+ {
+ std::unique_lock lock(_mutex);
+ before = erase_locked(tablet_id);
+ }
+ if (!before.has_value()) {
+ return;
+ }
+ bump_versions(before, std::nullopt);
+ persist(tablet_id, std::nullopt);
+}
+
+void TabletCachePolicyRegistry::restore(int64_t tablet_id, const
TabletCachePolicy& policy) {
+ std::optional<TabletCachePolicy> before;
+ {
+ std::unique_lock lock(_mutex);
+ if (_entries.contains(tablet_id)) {
+ // Already known from the live meta or from another cache path's
copy.
+ return;
+ }
+ // Never refreshed by this process, so it is due as soon as a cache
path reports it;
+ // counted as reported now so that it is not forgotten before the
cache has loaded.
+ before = set_locked(tablet_id, policy, 0);
+ _entries[tablet_id].reported_at_s = UnixSeconds();
+ }
+ bump_versions(before, policy);
+}
+
+void TabletCachePolicyRegistry::note_tablet(int64_t tablet_id) {
+ std::lock_guard pending_lock(_pending_mutex);
+ _noted.insert(tablet_id);
+}
+
+bool TabletCachePolicyRegistry::has_deadline_in(int64_t from, int64_t to)
const {
+ std::shared_lock lock(_mutex);
+ auto it = _deadlines.upper_bound(from);
+ return it != _deadlines.end() && *it <= to;
+}
+
+void TabletCachePolicyRegistry::report_cached_tablets(const
std::vector<int64_t>& tablet_ids) {
+ const int64_t now = UnixSeconds();
+ const int64_t stale_before = now -
config::file_cache_tablet_policy_refresh_interval_s;
+ std::vector<int64_t> due;
+ // In batches: a path may hold blocks of many tablets, and the readers
waiting on this lock
+ // hold their cache lock.
+ for (size_t begin = 0; begin < tablet_ids.size(); begin +=
kReportBatchSize) {
+ const size_t end = std::min(tablet_ids.size(), begin +
kReportBatchSize);
+ std::unique_lock lock(_mutex);
+ for (size_t i = begin; i < end; ++i) {
+ const int64_t tablet_id = tablet_ids[i];
+ auto it = _entries.find(tablet_id);
+ if (it == _entries.end()) {
+ due.push_back(tablet_id);
+ continue;
+ }
+ it->second.reported_at_s = now;
+ if (it->second.refreshed_at_s <= stale_before) {
+ due.push_back(tablet_id);
+ }
+ }
+ }
+ std::lock_guard pending_lock(_pending_mutex);
+ _due.insert(due.begin(), due.end());
+}
+
+std::vector<int64_t> TabletCachePolicyRegistry::take_due(int64_t now, size_t
budget) {
+ std::unordered_set<int64_t> noted;
+ {
+ std::lock_guard pending_lock(_pending_mutex);
+ noted.swap(_noted);
+ }
+ // A noted tablet may have been learned since.
+ std::erase_if(noted, [this](int64_t tablet_id) { return
deadline(tablet_id).has_value(); });
+ std::lock_guard pending_lock(_pending_mutex);
+ _due.insert(noted.begin(), noted.end());
+ std::vector<int64_t> batch;
+ for (auto it = _due.begin(); it != _due.end() && batch.size() < budget; it
= _due.erase(it)) {
+ auto retry = _retry_at.find(*it);
+ if (retry == _retry_at.end() || retry->second <= now) {
+ batch.push_back(*it);
+ }
+ }
+ return batch;
+}
+
+void TabletCachePolicyRegistry::prune(int64_t now) {
+ if (now < _next_prune_s) {
+ return;
+ }
+ _next_prune_s = now + kPruneIntervalS;
+ std::erase_if(_retry_at, [now](const auto& retry) { return retry.second <=
now; });
+ // A tablet no cache path holds and nothing refreshes (unloaded, or
dropped) has no blocks
+ // to judge; it is learned again if its blocks come back.
+ const int64_t forget_before = now - 2 *
config::file_cache_tablet_policy_refresh_interval_s;
Review Comment:
Fixed in a3d3e35279e: cache paths report their tablets while loading too,
and a policy is kept for at least five minutes without a report, whatever the
refresh interval.
##########
be/src/io/cache/cache_lru_dumper.cpp:
##########
@@ -486,7 +486,11 @@ void CacheLRUDumper::restore_queue(LRUQueue& queue, const
std::string& queue_nam
return;
}
// TODO(zhengyu): we don't use stats yet, see if this will cause
any problem
- _mgr->add_cell(hash, ctx, offset, size,
FileBlock::State::DOWNLOADED, cache_lock);
+ // Each block goes back to the queue it was dumped from. The dump
records neither its
+ // tablet nor its class; both are filled in when its metadata is
loaded, which also
+ // moves it to the queue they call for, see
BlockFileCache::complete_loaded_cell().
+ _mgr->add_cell_in_queue(hash, ctx, ctx.cache_type, offset, size,
Review Comment:
Fixed in bee475608cd: the test now checks the cache queue the blocks are
restored to, with three entries, their count and their order.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]