You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by ga...@apache.org on 2019/07/17 19:46:56 UTC

[trafficserver] branch master updated: Plugin reload

This is an automated email from the ASF dual-hosted git repository.

gancho pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 1eca389  Plugin reload
1eca389 is described below

commit 1eca389c5d8f908743d92b742d286b48b396a3a9
Author: Gancho Tenev <ga...@apache.org>
AuthorDate: Fri Mar 1 14:33:09 2019 -0800

    Plugin reload
    
    Reloading plugin allows new versions of a plugin code to be loaded
    and executed and old versions to be unloaded without restarting
    the traffic server process.
    
    More info in doc/developer-guide/plugins/reloading-plugins.en.rst
---
 .../api/functions/TSVConnCreate.en.rst             |  32 +
 doc/developer-guide/design-documents/index.en.rst  |  28 +
 .../design-documents/reloading-plugins.en.rst      | 178 ++++++
 doc/developer-guide/index.en.rst                   |   3 +-
 include/ts/InkAPIPrivateIOCore.h                   |   3 +-
 include/tscore/ts_file.h                           |  67 ++-
 proxy/ReverseProxy.cc                              |   4 +-
 proxy/http/HttpTransact.cc                         |   4 +-
 proxy/http/HttpTransact.h                          |   5 +-
 proxy/http/remap/Makefile.am                       |  87 +++
 proxy/http/remap/PluginDso.cc                      | 266 +++++++++
 proxy/http/remap/PluginDso.h                       | 104 ++++
 proxy/http/remap/PluginFactory.cc                  | 264 +++++++++
 proxy/http/remap/PluginFactory.h                   | 119 ++++
 proxy/http/remap/RemapConfig.cc                    | 201 ++-----
 proxy/http/remap/RemapPluginInfo.cc                | 256 ++++++--
 proxy/http/remap/RemapPluginInfo.h                 |  62 +-
 proxy/http/remap/RemapPlugins.cc                   |  18 +-
 proxy/http/remap/RemapPlugins.h                    |   7 +-
 proxy/http/remap/UrlMapping.cc                     |  50 +-
 proxy/http/remap/UrlMapping.h                      |  15 +-
 proxy/http/remap/UrlRewrite.cc                     |   4 +
 proxy/http/remap/UrlRewrite.h                      |   4 +
 proxy/http/remap/unit-tests/plugin_misc_cb.cc      | 106 ++++
 .../unit-tests/plugin_missing_deleteinstance.cc    |  57 ++
 .../remap/unit-tests/plugin_missing_doremap.cc     |  45 ++
 proxy/http/remap/unit-tests/plugin_missing_init.cc |  45 ++
 .../remap/unit-tests/plugin_missing_newinstance.cc |  56 ++
 proxy/http/remap/unit-tests/plugin_required_cb.cc  |  51 ++
 .../http/remap/unit-tests/plugin_testing_calls.cc  | 130 ++++
 .../http/remap/unit-tests/plugin_testing_common.cc |  39 ++
 .../http/remap/unit-tests/plugin_testing_common.h  |  95 +++
 proxy/http/remap/unit-tests/test_PluginDso.cc      | 395 +++++++++++++
 proxy/http/remap/unit-tests/test_PluginFactory.cc  | 657 +++++++++++++++++++++
 proxy/http/remap/unit-tests/test_RemapPlugin.cc    | 433 ++++++++++++++
 src/traffic_server/InkAPI.cc                       |  45 +-
 src/tscore/ts_file.cc                              | 210 +++++++
 src/tscore/unit_tests/test_ts_file.cc              | 193 ++++++
 38 files changed, 4058 insertions(+), 280 deletions(-)

diff --git a/doc/developer-guide/api/functions/TSVConnCreate.en.rst b/doc/developer-guide/api/functions/TSVConnCreate.en.rst
new file mode 100644
index 0000000..1e0cd50
--- /dev/null
+++ b/doc/developer-guide/api/functions/TSVConnCreate.en.rst
@@ -0,0 +1,32 @@
+.. 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:: ../../../common.defs
+
+.. default-domain:: c
+
+TSVConnCreate
+*************
+
+Synopsis
+========
+
+`#include <ts/ts.h>`
+
+.. function:: TSCont TSVConnCreate(TSEventFunc funcp, TSMutex mutexp)
+
+Description
+===========
diff --git a/doc/developer-guide/design-documents/index.en.rst b/doc/developer-guide/design-documents/index.en.rst
new file mode 100644
index 0000000..5d18ac0
--- /dev/null
+++ b/doc/developer-guide/design-documents/index.en.rst
@@ -0,0 +1,28 @@
+.. 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:: ../../common.defs
+
+.. _developer-design-documents:
+
+Design Documents
+****************
+
+.. toctree::
+   :maxdepth: 1
+
+   reloading-plugins.en
diff --git a/doc/developer-guide/design-documents/reloading-plugins.en.rst b/doc/developer-guide/design-documents/reloading-plugins.en.rst
new file mode 100644
index 0000000..c3cf61d
--- /dev/null
+++ b/doc/developer-guide/design-documents/reloading-plugins.en.rst
@@ -0,0 +1,178 @@
+.. 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:: ../../common.defs
+
+.. _developer-plugins-reloading-plugins:
+
+Reloading Plugins
+*****************
+
+Reloading plugin allows new versions of a plugin code to be loaded and executed and old versions to be unloaded without
+restarting the traffic server process.
+
+Plugins are Dynamic Shared Objects (DSO), new versions of the plugins are currently loaded by using a traffic server
+configuration reload, i.e.::
+
+  traffic_ctl config reload
+
+Although this feature should be transparent to there plugin developers, the following are some design considerations
+and implementation details.
+
+
+Design Considerations
+=====================
+
+1. The mechanism of the plugin reload should be transparent to the plugin developers, plugin developers should be
+   concerned only with properly initializing and cleaning up after the plugin and its instances.
+
+2. With the current traffic server implementation new version plugin (re)load is only triggered by a configuration
+   (re)load hence naturally the configuration should be always coupled with the set of plugins it loaded.
+
+3. Due to its asynchronouse nature traffic server should allow running different newer and older versions of the same plugin at the same time.
+
+4. Old plugin versions should be unloaded after the traffic server process no longer needs them after reload.
+
+5. Running different versions of the configuration and plugin versions at the same time requires maintaining
+   a "current active set" to be used by new transactions, new continuations, etc. and also multiple "previous inactive" sets as well.
+
+6. The result of the plugin reloading should be consistent across operating systems, file systems, dynamic loader
+   implementations.
+
+
+Currently only loading of "remap" plugins (`remap.config`) is supported but (re)loading of "global" plugins
+(`plugin.config`) could use same ideas and reuse some of the classes below.
+
+
+Consistent (re)loading behavior
+-------------------------------
+
+The following are some of the problems noticed during the initial experimentation:
+
+  a. There is an internal reference counting of the DSOs implemented inside the dynamic loader.
+     If an older version of the plugin DSO is still loaded then loading of a newer version of the DSO by using
+     the same filename does not load the new version.
+
+  b. If the filename used by the dynamic loader reference counting contains symbolic links the results are not
+     consistent across different operating/file systems and dynamic loader implementations.
+
+The following possible solutions were considered:
+
+  a. maintaining different plugin filenames for each version - this would put unnecessary burden on the
+     configuration management tools
+
+  b. experiments with Linux specific `dlmopen <http://man7.org/linux/man-pages/man3/dlopen.3.html>`_ yielded
+     good results but it was not available on all supported platforms
+
+
+A less efficient but more reliable solution was chosen - DSO files are temporarily copied to and consequently
+loaded from a runtime location and the copies is kept until plugin is unloaded.
+
+Each configuration / plugin reload would use a different runtime location, ``ATSUuid`` is used to create unique
+runtime directories.
+
+
+Reference counting against DSOs
+-------------------------------
+
+During the initial analysis a common sense solution was considered - to add instrumentation around handling
+of registered hooks in order to unload plugins safely. This would be more involved and not sufficient since hooks
+are not the only mechanism that relies on the plugin DSO being loaded. This design / implementation proposes
+a different approach.
+
+Plugin code can be called from HTTP state machine (1) while handling HTTP transactions or (2) while calling
+event handling functions of continuations created by the plugin code.
+The plugin reload mechanism should guarantee that all necessary plugin DSOs are still loaded when those calls
+are performed.
+
+Those continuations are created by :c:func:`TSContCreate` and :c:func:`TSVConnCreate` and
+could be used for registering hooks (i.e. registered by :c:func:`TSHttpHookAdd`) or for
+scheduling events in the future (i.e. :c:func:`TSContScheduleOnPool`).
+
+
+Registering hooks always requires creating continuations from inside the plugin code and a separate
+instrumentation around handling of hooks is not necessary.
+
+There is an existing reference counting around ``UrlRewrite`` which makes sure it stays loaded until the HTTP state
+machine (the last HTTP transaction) stops using it. By making all plugins loaded by a single configuration reload
+a part of ``UrlRewrite`` (see `PluginFactory`_ below), we could guarantee the plugins are always loaded while
+in use by the HTTP transactions.
+
+
+Plugin context
+--------------
+
+Reference counting and managing different configuration and plugin sets require the continuation creation and
+destruction to know in which plugin context they are running.
+
+Traffic server API change was considered for ``TSCreateCont``, ``TSVConnCreate`` and ``TSDestroyCont`` but
+it was decided to keep things hidden from the plugin developer by using thread local plugin context which
+would be set/switched accordingly before executing the plugin code.
+
+The continuations created by the plugin will have a context member added to them which will be used by
+the reference counting and when continuations are destroyed or handle events.
+
+
+TSHttpArgs
+----------
+
+Traffic Server sessions and transactions provide a fixed array of void pointers that can be used by plugins
+to store information. To avoid collisions between plugins a plugin should first *reserve* an index in the array.
+
+Since :c:func:`TSHttpTxnArgIndexReserve` and :c:func:`TSHttpSsnArgIndexReserve` are meant to be called during plugin
+initialization we could end up "leaking" indices during plugins reload.
+Hence it is necessary to make sure only one index is allocated per "plugin identifying name", current
+:c:func:`TSHttpTxnArgIndexNameLookup` and :c:func:`TSHttpTxnArgIndexNameLookup` implementation assumes 1-1
+index-to-name relationship as well.
+
+
+PluginFactory
+-------------
+
+`PluginFactory` - creates and holds all plugin instances corresponding to a single configuration (re)load.
+
+#. Instantiates and initializes 'remap' plugins, eventually signals plugin unload/destruction, makes sure each plugin
+   version is loaded only once per configuration (re)load by maintaining a list of DSOs already loaded.
+
+#. Initializes, keeps track of all resulting plugin instances and eventually signals each instance destruction.
+
+#. Handles multiple plugin search paths.
+
+#. Sets a common runtime path for all plugins loaded in a single configuration (re)load to guarantee
+   `consistent (re)loading behavior`_.
+
+
+
+RemapPluginInfo
+---------------
+
+`RemapPluginInfo` - a class representing a 'remap' plugin, derived from `PluginDso`, and handling 'remap' plugin specific
+initialization and destruction and also sets up the right plugin context when its methods are called.
+
+
+
+PluginDso
+---------
+
+`PluginDso` - a class performing the actual DSO loading and unloading and all related initialization and
+cleanup plus related error handling. Its functionality is modularized into a separate class in hopes to
+be reused by 'global' plugins in the future.
+
+
+To make sure plugins are still loaded while their code is still in use there is reference counting done around ``PluginDso``
+which inherits ``RefCountObj`` and implements ``aqcuire()`` and ``release()`` methods which are called by ``TSCreateCont``,
+``TSVConnCreate`` and ``TSDestroyCont``.
diff --git a/doc/developer-guide/index.en.rst b/doc/developer-guide/index.en.rst
index 5deda01..d21e840 100644
--- a/doc/developer-guide/index.en.rst
+++ b/doc/developer-guide/index.en.rst
@@ -55,4 +55,5 @@ duplicate bugs is encouraged, but not required.
    host-resolution-proposal.en
    client-session-architecture.en
    core-architecture/index.en
-   layout/index.en
\ No newline at end of file
+   design-documents/index.en
+   layout/index.en
diff --git a/include/ts/InkAPIPrivateIOCore.h b/include/ts/InkAPIPrivateIOCore.h
index baebdc4..77283fd 100644
--- a/include/ts/InkAPIPrivateIOCore.h
+++ b/include/ts/InkAPIPrivateIOCore.h
@@ -43,7 +43,7 @@ public:
   INKContInternal();
   INKContInternal(TSEventFunc funcp, TSMutex mutexp);
 
-  void init(TSEventFunc funcp, TSMutex mutexp);
+  void init(TSEventFunc funcp, TSMutex mutexp, void *context = 0);
   virtual void destroy();
 
   void handle_event_count(int event);
@@ -60,6 +60,7 @@ public:
   int m_closed;
   int m_deletable;
   int m_deleted;
+  void *m_context;
   // INKqa07670: Nokia memory leak bug fix
   INKContInternalMagic_t m_free_magic;
 };
diff --git a/include/tscore/ts_file.h b/include/tscore/ts_file.h
index b5ec194..e9bfd8b 100644
--- a/include/tscore/ts_file.h
+++ b/include/tscore/ts_file.h
@@ -104,6 +104,12 @@ namespace file
     /// Get a copy of the path.
     std::string string() const;
 
+    /// Get relative path
+    self_type relative_path();
+
+    /// Get parent path
+    path parent_path();
+
   protected:
     std::string _path; ///< File path.
   };
@@ -120,6 +126,7 @@ namespace file
     friend self_type status(const path &, std::error_code &) noexcept;
 
     friend int file_type(const self_type &);
+    friend time_t modification_time(const file_status &fs);
     friend uintmax_t file_size(const self_type &);
     friend bool is_regular_file(const file_status &);
     friend bool is_dir(const file_status &);
@@ -141,6 +148,9 @@ namespace file
   /// Return the file type value.
   int file_type(const file_status &fs);
 
+  /// Return modification time
+  time_t modification_time(const file_status &fs);
+
   /// Check if the path is to a regular file.
   bool is_regular_file(const file_status &fs);
 
@@ -159,7 +169,28 @@ namespace file
   /// Check if file is readable.
   bool is_readable(const path &s);
 
-  /** Load the file at @a p into a @c std::string.
+  // Get directory location suitable for temporary files
+  path temp_directory_path();
+
+  // Returns current path.
+  path current_path();
+
+  // Returns return the canonicalized absolute pathname
+  path canonical(const path &p, std::error_code &ec);
+
+  // Checks if the file/directory exists
+  bool exists(const path &p);
+
+  // Create directories
+  bool create_directories(const path &p, std::error_code &ec, mode_t mode = 0775) noexcept;
+
+  // Copy files ("from" cannot be directory). @todo make it more generic
+  bool copy(const path &from, const path &to, std::error_code &ec);
+
+  // Removes files and directories recursively
+  bool remove(const path &path, std::error_code &ec);
+
+  /** Load the file at @a p into a @c std::string
    *
    * @param p Path to file
    * @return The contents of the file.
@@ -222,6 +253,28 @@ namespace file
     return *this /= std::string_view(that._path);
   }
 
+  inline path
+  path::relative_path()
+  {
+    return (this->is_absolute()) ? path(_path.substr(sizeof(preferred_separator))) : *this;
+  }
+
+  inline path
+  path::parent_path()
+  {
+    if (this->is_absolute() && _path.substr(sizeof(preferred_separator)).empty()) {
+      // No relative path
+      return *this;
+    }
+
+    const size_t last_slash_idx = _path.find_last_of(preferred_separator);
+    if (std::string::npos != last_slash_idx) {
+      return path(_path.substr(0, last_slash_idx));
+    } else {
+      return path();
+    }
+  }
+
   /** Combine two strings as file paths.
 
        @return A @c path with the combined path.
@@ -250,6 +303,18 @@ namespace file
     return path(std::move(lhs)) /= rhs;
   }
 
+  inline bool
+  operator==(const path &lhs, const path &rhs)
+  {
+    return lhs.string() == rhs.string();
+  }
+
+  inline bool
+  operator!=(const path &lhs, const path &rhs)
+  {
+    return lhs.string() != rhs.string();
+  }
+
   /* ------------------------------------------------------------------- */
 } // namespace file
 } // namespace ts
diff --git a/proxy/ReverseProxy.cc b/proxy/ReverseProxy.cc
index 2ab3d03..a4751a3 100644
--- a/proxy/ReverseProxy.cc
+++ b/proxy/ReverseProxy.cc
@@ -43,7 +43,8 @@
 
 // Global Ptrs
 static Ptr<ProxyMutex> reconfig_mutex;
-UrlRewrite *rewrite_table = nullptr;
+UrlRewrite *rewrite_table                             = nullptr;
+thread_local PluginThreadContext *pluginThreadContext = nullptr;
 
 // Tokens for the Callback function
 #define FILE_CHANGED 0
@@ -150,6 +151,7 @@ reloadUrlRewrite()
     ink_assert(oldTable != nullptr);
 
     // Release the old one
+    oldTable->pluginFactory.indicateReload();
     oldTable->release();
 
     Debug("url_rewrite", "%s", msg);
diff --git a/proxy/http/HttpTransact.cc b/proxy/http/HttpTransact.cc
index 854755b..a19fe40 100644
--- a/proxy/http/HttpTransact.cc
+++ b/proxy/http/HttpTransact.cc
@@ -3492,8 +3492,8 @@ HttpTransact::handle_response_from_server(State *s)
 
   // plugin call
   s->server_info.state = s->current.state;
-  if (s->fp_tsremap_os_response) {
-    s->fp_tsremap_os_response(s->remap_plugin_instance, reinterpret_cast<TSHttpTxn>(s->state_machine), s->current.state);
+  if (s->os_response_plugin_inst) {
+    s->os_response_plugin_inst->osResponse(reinterpret_cast<TSHttpTxn>(s->state_machine), s->current.state);
   }
 
   switch (s->current.state) {
diff --git a/proxy/http/HttpTransact.h b/proxy/http/HttpTransact.h
index 4889da9..a1f5b47 100644
--- a/proxy/http/HttpTransact.h
+++ b/proxy/http/HttpTransact.h
@@ -766,10 +766,9 @@ public:
     CacheAuth_t www_auth_content = CACHE_AUTH_NONE;
 
     // INK API/Remap API plugin interface
-    void *remap_plugin_instance = nullptr;
     void *user_args[TS_HTTP_MAX_USER_ARG];
-    RemapPluginInfo::OS_Response_F *fp_tsremap_os_response = nullptr;
-    HTTPStatus http_return_code                            = HTTP_STATUS_NONE;
+    RemapPluginInst *os_response_plugin_inst = nullptr;
+    HTTPStatus http_return_code              = HTTP_STATUS_NONE;
 
     int api_txn_active_timeout_value      = -1;
     int api_txn_connect_timeout_value     = -1;
diff --git a/proxy/http/remap/Makefile.am b/proxy/http/remap/Makefile.am
index ddee9dc..3a10195 100644
--- a/proxy/http/remap/Makefile.am
+++ b/proxy/http/remap/Makefile.am
@@ -22,6 +22,7 @@ AM_CPPFLAGS += \
 	$(iocore_include_dirs) \
 	-I$(abs_top_srcdir)/include \
 	-I$(abs_top_srcdir)/lib \
+	-I$(abs_top_srcdir)/lib/records \
 	-I$(abs_top_srcdir)/proxy \
 	-I$(abs_top_srcdir)/mgmt \
 	-I$(abs_top_srcdir)/mgmt/utils \
@@ -39,6 +40,9 @@ libhttp_remap_a_SOURCES = \
 	RemapConfig.h \
 	RemapPluginInfo.cc \
 	RemapPluginInfo.h \
+	PluginDso.cc \
+	PluginFactory.cc \
+	PluginFactory.h \
 	RemapPlugins.cc \
 	RemapPlugins.h \
 	RemapProcessor.cc \
@@ -52,3 +56,86 @@ libhttp_remap_a_SOURCES = \
 
 clang-tidy-local: $(libhttp_remap_a_SOURCES)
 	$(CXX_Clang_Tidy)
+
+TESTS = $(check_PROGRAMS)
+check_PROGRAMS =  test_PluginDso test_PluginFactory test_RemapPluginInfo
+
+test_PluginDso_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include -DPLUGIN_DSO_TESTS
+EXTRA_test_PluginDso_DEPENDENCIES = unit-tests/plugin_v1.la
+test_PluginDso_LDADD = $(OPENSSL_LIBS)
+test_PluginDso_LDFLAGS = $(AM_LDFLAGS) -L$(top_builddir)/src/tscore/.libs -ltscore
+test_PluginDso_SOURCES = \
+	unit-tests/test_PluginDso.cc \
+	unit-tests/plugin_testing_common.cc \
+	PluginDso.cc
+
+test_PluginFactory_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include -DPLUGIN_DSO_TESTS
+EXTRA_test_PluginFactory_DEPENDENCIES = unit-tests/plugin_v1.la
+test_PluginFactory_LDADD = $(OPENSSL_LIBS)
+test_PluginFactory_LDFLAGS = $(AM_LDFLAGS) -L$(top_builddir)/src/tscore/.libs -ltscore
+test_PluginFactory_SOURCES = \
+	unit-tests/test_PluginFactory.cc \
+	unit-tests/plugin_testing_common.cc \
+	PluginFactory.cc \
+	PluginDso.cc \
+	RemapPluginInfo.cc
+
+test_RemapPluginInfo_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include -DPLUGIN_DSO_TESTS
+	EXTRA_test_RemapPluginInfo_DEPENDENCIES = \
+	unit-tests/plugin_missing_init.la \
+	unit-tests/plugin_missing_doremap.la \
+	unit-tests/plugin_missing_deleteinstance.la \
+	unit-tests/plugin_required_cb.la \
+	unit-tests/plugin_missing_newinstance.la \
+	unit-tests/plugin_testing_calls.la
+test_RemapPluginInfo_LDADD = \
+	$(OPENSSL_LIBS)
+test_RemapPluginInfo_LDFLAGS = $(AM_LDFLAGS) -L$(top_builddir)/src/tscore/.libs -ltscore
+test_RemapPluginInfo_SOURCES = \
+	unit-tests/plugin_testing_common.cc \
+	unit-tests/plugin_testing_calls.cc \
+	unit-tests/test_RemapPlugin.cc \
+	PluginDso.cc \
+	RemapPluginInfo.cc
+
+
+DSO_LDFLAGS = \
+	-module \
+	-shared \
+	-avoid-version \
+	-export-symbols-regex '^(TSRemapInit|TSRemapDone|TSRemapDoRemap|TSRemapNewInstance|TSRemapDeleteInstance|TSRemapOSResponse|TSRemapConfigReload|TSPluginInit|pluginDsoVersionTest|getPluginDebugObjectTest)$$'
+
+# Build plugins for unit testing the plugin (re)load.
+pkglib_LTLIBRARIES = \
+	unit-tests/plugin_v1.la \
+	unit-tests/plugin_v2.la \
+	unit-tests/plugin_required_cb.la \
+	unit-tests/plugin_missing_deleteinstance.la \
+	unit-tests/plugin_missing_doremap.la \
+	unit-tests/plugin_missing_init.la \
+	unit-tests/plugin_missing_newinstance.la \
+	unit-tests/plugin_testing_calls.la
+unit_tests_plugin_v1_la_SOURCES = unit-tests/plugin_misc_cb.cc
+unit_tests_plugin_v1_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_v1_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS)
+unit_tests_plugin_v2_la_SOURCES = unit-tests/plugin_misc_cb.cc
+unit_tests_plugin_v2_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_v2_la_CPPFLAGS = -DPLUGINDSOVER=2 $(AM_CPPFLAGS)
+unit_tests_plugin_required_cb_la_SOURCES = unit-tests/plugin_required_cb.cc
+unit_tests_plugin_required_cb_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_required_cb_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS)
+unit_tests_plugin_missing_deleteinstance_la_SOURCES = unit-tests/plugin_missing_deleteinstance.cc
+unit_tests_plugin_missing_deleteinstance_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_missing_deleteinstance_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS)
+unit_tests_plugin_missing_doremap_la_SOURCES = unit-tests/plugin_missing_doremap.cc
+unit_tests_plugin_missing_doremap_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_missing_doremap_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS)
+unit_tests_plugin_missing_init_la_SOURCES = unit-tests/plugin_missing_init.cc
+unit_tests_plugin_missing_init_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_missing_init_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS)
+unit_tests_plugin_missing_newinstance_la_SOURCES = unit-tests/plugin_missing_newinstance.cc
+unit_tests_plugin_missing_newinstance_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_missing_newinstance_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS)
+unit_tests_plugin_testing_calls_la_SOURCES = unit-tests/plugin_testing_calls.cc unit-tests/plugin_testing_common.cc
+unit_tests_plugin_testing_calls_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_testing_calls_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS)
diff --git a/proxy/http/remap/PluginDso.cc b/proxy/http/remap/PluginDso.cc
new file mode 100644
index 0000000..24490fc
--- /dev/null
+++ b/proxy/http/remap/PluginDso.cc
@@ -0,0 +1,266 @@
+/** @file
+
+  A class that deals with plugin Dynamic Shared Objects (DSO)
+
+  @section license License
+
+  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.
+
+  @section details Details
+
+  Implements code necessary for Reverse Proxy which mostly consists of
+  general purpose hostname substitution in URLs.
+
+ */
+
+#include "PluginDso.h"
+#ifdef PLUGIN_DSO_TESTS
+#include "unit-tests/plugin_testing_common.h"
+#else
+#include "tscore/Diags.h"
+#endif
+
+PluginDso::PluginDso(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath)
+  : _configPath(configPath), _effectivePath(effectivePath), _runtimePath(runtimePath)
+{
+}
+
+PluginDso::~PluginDso()
+{
+  std::string error;
+  (void)unload(error);
+}
+
+bool
+PluginDso::load(std::string &error)
+{
+  /* Clear all errors */
+  error.clear();
+  _errorCode.clear();
+  bool result = true;
+
+  if (isLoaded()) {
+    error.append("plugin already loaded");
+    return false;
+  }
+
+  Debug(_tag, "plugin '%s' started loading DSO", _configPath.c_str());
+
+  /* Find plugin DSO looking through the search dirs */
+  if (_effectivePath.empty()) {
+    error.append("empty effective path");
+    result = false;
+  } else {
+    Debug(_tag, "plugin '%s' effective path: %s", _configPath.c_str(), _effectivePath.c_str());
+
+    /* Copy the installed plugin DSO to a runtime directory */
+    std::error_code ec;
+    if (!copy(_effectivePath, _runtimePath, ec)) {
+      std::string temp_error;
+      temp_error.append("failed to create a copy: ").append(strerror(ec.value()));
+      error.assign(temp_error);
+      result = false;
+    } else {
+      Debug(_tag, "plugin '%s' runtime path: %s", _configPath.c_str(), _runtimePath.c_str());
+
+      /* Save the time for later checking if DSO got modified in consecutive DSO reloads */
+      std::error_code ec;
+      fs::file_status fs = fs::status(_effectivePath, ec);
+      _mtime             = fs::modification_time(fs);
+      Debug(_tag, "plugin '%s' mоdification time %ld", _configPath.c_str(), _mtime);
+
+      /* Now attemt to load the plugin DSO */
+      if ((_dlh = dlopen(_runtimePath.c_str(), RTLD_NOW)) == nullptr) {
+#if defined(freebsd) || defined(openbsd)
+        char *err = (char *)dlerror();
+#else
+        char *err = dlerror();
+#endif
+        error.append(err ? err : "Unknown dlopen() error");
+        _dlh = nullptr; /* mark that the constructor failed. */
+
+        clean(error);
+        result = false;
+
+        Error("plugin '%s' failed to load: %s", _configPath.c_str(), error.c_str());
+      }
+    }
+
+    /* Remove the runtime DSO copy even if we succeed loading to avoid leftovers after crashes */
+    if (_preventiveCleaning) {
+      clean(error);
+    }
+  }
+  Debug(_tag, "plugin '%s' finished loading DSO", _configPath.c_str());
+
+  return result;
+}
+
+/**
+ * @brief unload plugin DSO
+ *
+ * @param error - error messages in case of failure.
+ * @return true - success, false - failure during unload.
+ */
+bool
+PluginDso::unload(std::string &error)
+{
+  /* clean errors */
+  error.clear();
+  bool result = false;
+
+  if (isLoaded()) {
+    result = (0 == dlclose(_dlh));
+    _dlh   = nullptr;
+    if (true == result) {
+      clean(error);
+    } else {
+      error.append("failed to unload plugin");
+    }
+  } else {
+    error.append("no plugin loaded");
+    result = false;
+  }
+
+  return result;
+}
+
+/**
+ * @brief returns the address of a symbol in the plugin DSO
+ *
+ * @param symbol symbol name
+ * @param address reference to the address to be returned to the caller
+ * @param error error messages in case of symbol is not found
+ * @return true if success, false could not find the symbol (symbol can be nullptr itself)
+ */
+bool
+PluginDso::getSymbol(const char *symbol, void *&address, std::string &error) const
+{
+  /* Clear the errors */
+  dlerror();
+  error.clear();
+
+  address   = dlsym(_dlh, symbol);
+  char *err = dlerror();
+
+  if (nullptr == address && nullptr != err) {
+    /* symbol really cannot be found */
+    error.assign(err);
+    return false;
+  }
+
+  return true;
+}
+
+/**
+ * @brief shows if the DSO corresponding to this effective path has already been loaded.
+ * @return true - loaded, false - not loaded
+ */
+bool
+PluginDso::isLoaded()
+{
+  return nullptr != _dlh;
+}
+
+/**
+ * @brief full path to the first plugin found in the search path which will be used to be loaded.
+ *
+ * @return full path to the plugin DSO.
+ */
+const fs::path &
+PluginDso::effectivePath() const
+{
+  return _effectivePath;
+}
+
+/**
+ * @brief full path to the runtime location of the plugin DSO actually loaded.
+ *
+ * @return full path to the runtime plugin DSO.
+ */
+
+const fs::path &
+PluginDso::runtimePath() const
+{
+  return _runtimePath;
+}
+
+/**
+ * @brief DSO modification time at the moment of DSO load.
+ *
+ * @return modification time.
+ */
+
+time_t
+PluginDso::modTime() const
+{
+  return _mtime;
+}
+
+/**
+ * @brief clean files created by the plugin instance and handle errors
+ *
+ * @param error a human readable error message if something goes wrong
+ * @ return void
+ */
+void
+PluginDso::clean(std::string &error)
+{
+  if (false == remove(_runtimePath, _errorCode)) {
+    error.append("failed to remove runtime copy: ").append(_errorCode.message());
+  }
+}
+
+void
+PluginDso::acquire()
+{
+  this->refcount_inc();
+  Debug(_tag, "plugin DSO acquire (ref-count:%d, dso-addr:%p)", this->refcount(), this);
+}
+
+void
+PluginDso::release()
+{
+  Debug(_tag, "plugin DSO release (ref-count:%d, dso-addr:%p)", this->refcount() - 1, this);
+  if (0 == this->refcount_dec()) {
+    Debug(_tag, "unloading plugin DSO '%s' (dso-addr:%p)", _configPath.c_str(), this);
+    _list.erase(this);
+    delete this;
+  }
+}
+
+void
+PluginDso::incInstanceCount()
+{
+  _instanceCount.refcount_inc();
+  Debug(_tag, "instance count (inst-count:%d, dso-addr:%p)", _instanceCount.refcount(), this);
+}
+
+void
+PluginDso::decInstanceCount()
+{
+  _instanceCount.refcount_dec();
+  Debug(_tag, "instance count (inst-count:%d, dso-addr:%p)", _instanceCount.refcount(), this);
+}
+
+int
+PluginDso::instanceCount()
+{
+  return _instanceCount.refcount();
+}
+
+PluginDso::PluginList PluginDso::_list;
diff --git a/proxy/http/remap/PluginDso.h b/proxy/http/remap/PluginDso.h
new file mode 100644
index 0000000..4554c6b
--- /dev/null
+++ b/proxy/http/remap/PluginDso.h
@@ -0,0 +1,104 @@
+/** @file
+
+  Header file for a class that deals with plugin Dynamic Shared Objects (DSO)
+
+  @section license License
+
+  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.
+
+  @section details Details
+
+  Implements code necessary for Reverse Proxy which mostly consists of
+  general purpose hostname substitution in URLs.
+
+ */
+
+#pragma once
+
+#include <dlfcn.h>
+#include <vector>
+#include <ctime>
+
+#include "tscore/ts_file.h"
+namespace fs = ts::file;
+
+#include "tscore/Ptr.h"
+#include "tscpp/util/IntrusiveDList.h"
+
+class PluginThreadContext : public RefCountObj
+{
+public:
+  virtual void acquire()                  = 0;
+  virtual void release()                  = 0;
+  static constexpr const char *const _tag = "plugin_context"; /** @brief log tag used by this class */
+};
+
+class PluginDso : public PluginThreadContext
+{
+  friend class PluginFactory;
+
+public:
+  PluginDso(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath);
+  virtual ~PluginDso();
+
+  /* DSO Load, unload, get symbols from DSO */
+  virtual bool load(std::string &error);
+  virtual bool unload(std::string &error);
+  bool isLoaded();
+  bool getSymbol(const char *symbol, void *&address, std::string &error) const;
+
+  /* Accessors for effective and runtime paths */
+  const fs::path &effectivePath() const;
+  const fs::path &runtimePath() const;
+  time_t modTime() const;
+
+  /* List used by the plugin factory */
+  using self_type  = PluginDso; ///< Self reference type.
+  self_type *_next = nullptr;
+  self_type *_prev = nullptr;
+  using Linkage    = ts::IntrusiveLinkage<self_type>;
+  using PluginList = ts::IntrusiveDList<PluginDso::Linkage>;
+
+  /* Methods to be called when processing a list of plugins, to overloaded by the remap or the global plugins correspondingly */
+  virtual void indicateReload()         = 0;
+  virtual bool init(std::string &error) = 0;
+  virtual void done()                   = 0;
+
+  void acquire();
+  void release();
+
+  void incInstanceCount();
+  void decInstanceCount();
+  int instanceCount();
+
+protected:
+  void clean(std::string &error);
+
+  fs::path _configPath;    /** @brief the name specified in the config file */
+  fs::path _effectivePath; /** @brief the plugin installation path which was used to load DSO */
+  fs::path _runtimePath;   /** @brief the plugin runtime path where the plugin was copied to be loaded */
+
+  void *_dlh = nullptr; /** @brief dlopen handler used internally in this class, used as flag for loaded vs unloaded (nullptr) */
+  std::error_code _errorCode; /** @brief used in filesystem calls */
+
+  static constexpr const char *const _tag = "plugin_dso"; /** @brief log tag used by this class */
+  time_t _mtime                           = 0;            /* @brief modification time of the DSO's file, used for checking */
+  bool _preventiveCleaning                = true;
+
+  static PluginList _list; /** @brief a global list of plugins, usually maintained by a plugin factory or plugin instance itself */
+  RefCountObj _instanceCount; /** @brief used for properly calling "done" and "indicate config reload" methods by the factory */
+};
diff --git a/proxy/http/remap/PluginFactory.cc b/proxy/http/remap/PluginFactory.cc
new file mode 100644
index 0000000..c6b1c8a
--- /dev/null
+++ b/proxy/http/remap/PluginFactory.cc
@@ -0,0 +1,264 @@
+/** @file
+
+  Functionality allowing to load all plugins from a single config reload.
+
+  @section license License
+
+  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 "RemapPluginInfo.h"
+#include "PluginFactory.h"
+#ifdef PLUGIN_DSO_TESTS
+#include "unit-tests/plugin_testing_common.h"
+#else
+#include "tscore/Diags.h"
+#endif
+
+#include <algorithm> /* std::swap */
+
+RemapPluginInst::RemapPluginInst(RemapPluginInfo &plugin) : _plugin(plugin)
+{
+  _plugin.acquire();
+  _plugin.incInstanceCount();
+}
+
+RemapPluginInst::~RemapPluginInst()
+{
+  _plugin.decInstanceCount();
+  _plugin.release();
+}
+
+bool
+RemapPluginInst::init(int argc, char **argv, std::string &error)
+{
+  bool result = false;
+  result      = _plugin.initInstance(argc, argv, &_instance, error);
+
+  return result;
+}
+
+void
+RemapPluginInst::done()
+{
+  _plugin.doneInstance(_instance);
+}
+
+TSRemapStatus
+RemapPluginInst::doRemap(TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+  return _plugin.doRemap(_instance, rh, rri);
+}
+
+void
+RemapPluginInst::osResponse(TSHttpTxn rh, int os_response_type)
+{
+  _plugin.osResponse(_instance, rh, os_response_type);
+}
+
+PluginFactory::PluginFactory()
+{
+  _uuid = new ATSUuid();
+  if (nullptr != _uuid) {
+    _uuid->initialize(TS_UUID_V4);
+    if (!_uuid->valid()) {
+      /* Destroy and mark failure */
+      delete _uuid;
+      _uuid = nullptr;
+    }
+  }
+
+  Debug(_tag, "created plugin factory %s", getUuid());
+}
+
+PluginFactory::~PluginFactory()
+{
+  _instList.apply([](RemapPluginInst *pluginInst) -> void { delete pluginInst; });
+  _instList.clear();
+
+  fs::remove(_runtimeDir, _ec);
+
+  Debug(_tag, "destroyed plugin factory %s", getUuid());
+  delete _uuid;
+}
+
+PluginFactory &
+PluginFactory::addSearchDir(const fs::path &searchDir)
+{
+  _searchDirs.push_back(searchDir);
+  Debug(_tag, "added plugin search dir %s", searchDir.c_str());
+  return *this;
+}
+
+PluginFactory &
+PluginFactory::setRuntimeDir(const fs::path &runtimeDir)
+{
+  _runtimeDir = runtimeDir / fs::path(getUuid());
+  Debug(_tag, "set plugin runtime dir %s", runtimeDir.c_str());
+  return *this;
+}
+
+const char *
+PluginFactory::getUuid()
+{
+  return _uuid ? _uuid->getString() : "uknown";
+}
+
+/**
+ * @brief Loads, initializes and return a valid Remap Plugin instance.
+ *
+ * @param configPath plugin path as specified in the plugin
+ * @param argc number of parameters passed to the plugin during instance initialization
+ * @param argv parameters passed to the plugin during instance initialization
+ * @param context Plugin context is used from continuations to guarantee correct reference counting against the plugin.
+ * @param error human readable message if something goes wrong, empty otherwise
+ * @return pointer to a plugin instance, nullptr if failure
+ */
+RemapPluginInst *
+PluginFactory::getRemapPlugin(const fs::path &configPath, int argc, char **argv, std::string &error)
+{
+  /* Discover the effective path by looking into the search dirs */
+  fs::path effectivePath = getEffectivePath(configPath);
+  if (effectivePath.empty()) {
+    error.assign("failed to find plugin '").append(configPath.string()).append("'");
+    return nullptr;
+  }
+
+  /* Only one plugin with this effective path can be loaded by a plugin factory */
+  RemapPluginInfo *plugin = dynamic_cast<RemapPluginInfo *>(findByEffectivePath(effectivePath));
+  RemapPluginInst *inst   = nullptr;
+
+  if (nullptr == plugin) {
+    /* The plugin requested have not been loaded yet. */
+    Debug(_tag, "plugin '%s' has not been loaded yet, loading as remap plugin", configPath.c_str());
+
+    fs::path runtimePath;
+    runtimePath /= _runtimeDir;
+    runtimePath /= effectivePath.relative_path();
+
+    fs::path parent = runtimePath.parent_path();
+    if (!fs::create_directories(parent, _ec)) {
+      error.assign("failed to create plugin runtime dir");
+      return nullptr;
+    }
+
+    plugin = new RemapPluginInfo(configPath, effectivePath, runtimePath);
+    if (nullptr != plugin) {
+      if (plugin->load(error)) {
+        _list.append(plugin);
+
+        if (plugin->init(error)) {
+          inst = new RemapPluginInst(*plugin);
+          inst->init(argc, argv, error);
+          _instList.append(inst);
+        }
+
+        if (_preventiveCleaning) {
+          clean(error);
+        }
+      } else {
+        return nullptr;
+      }
+    }
+  } else {
+    Debug(_tag, "plugin '%s' has already been loaded", configPath.c_str());
+    inst = new RemapPluginInst(*plugin);
+    inst->init(argc, argv, error);
+    _instList.append(inst);
+  }
+
+  return inst;
+}
+
+/**
+ * @brief full path to the first plugin found in the search path which will be used to be copied to runtime location and loaded.
+ *
+ * @param configPath path specified in the config file, it can be relative path.
+ * @return full path to the plugin.
+ */
+fs::path
+PluginFactory::getEffectivePath(const fs::path &configPath)
+{
+  if (configPath.is_absolute()) {
+    if (fs::exists(configPath)) {
+      return fs::canonical(configPath.string(), _ec);
+    } else {
+      return fs::path();
+    }
+  }
+
+  fs::path path;
+
+  for (auto dir : _searchDirs) {
+    fs::path candidatePath = dir / configPath;
+    if (fs::exists(candidatePath)) {
+      path = fs::canonical(candidatePath, _ec);
+      break;
+    }
+  }
+
+  return path;
+}
+
+/**
+ * @brief Find a plugin by path from our linked plugin list by using plugin effective (canonical) path
+ *
+ * @param path effective (caninical) path
+ * @return plugin found or nullptr if not found
+ */
+PluginDso *
+PluginFactory::findByEffectivePath(const fs::path &path)
+{
+  struct stat sb;
+  time_t mtime = 0;
+  if (0 == stat(path.c_str(), &sb)) {
+    mtime = sb.st_mtime;
+  }
+  auto spot = std::find_if(_list.begin(), _list.end(), [&](PluginDso const &plugin) -> bool {
+    return (0 == path.string().compare(plugin.effectivePath().string()) && (mtime == plugin.modTime()));
+  });
+  return spot == _list.end() ? nullptr : static_cast<PluginDso *>(spot);
+}
+
+/**
+ * @brief Tell all plugins (that so wish) that remap.config is being reloaded
+ *
+ * This method would be useful only in case configs are reloaded independently from
+ * factory/plugins instantiation and initialization.
+ */
+void
+PluginFactory::indicateReload()
+{
+  Debug(_tag, "indicated config reload to factory '%s'", getUuid());
+
+  _instList.apply([](RemapPluginInst &pluginInst) -> void { pluginInst.done(); });
+
+  _list.apply([](PluginDso &plugin) -> void {
+    if (1 == plugin.instanceCount()) {
+      plugin.done();
+    } else {
+      plugin.indicateReload();
+    }
+  });
+}
+
+void
+PluginFactory::clean(std::string &error)
+{
+  fs::remove(_runtimeDir, _ec);
+}
diff --git a/proxy/http/remap/PluginFactory.h b/proxy/http/remap/PluginFactory.h
new file mode 100644
index 0000000..1cb0661
--- /dev/null
+++ b/proxy/http/remap/PluginFactory.h
@@ -0,0 +1,119 @@
+/** @file
+
+  Functionality allowing to load all plugins from a single config reload (header).
+
+  @section license License
+
+  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.
+ */
+
+#pragma once
+
+#include <vector>
+
+#include "tscore/Ptr.h"
+#include "PluginDso.h"
+#include "RemapPluginInfo.h"
+
+#include "tscore/Ptr.h"
+#include "tscpp/util/IntrusiveDList.h"
+
+#include "tscore/ink_uuid.h"
+#include "ts/apidefs.h"
+
+/**
+ * @brief Bundles plugin info + plugin instance data to be used always together.
+ */
+class RemapPluginInst
+{
+public:
+  RemapPluginInst()                  = delete;
+  RemapPluginInst(RemapPluginInst &) = delete;
+  RemapPluginInst(RemapPluginInfo &plugin);
+  ~RemapPluginInst();
+
+  /* Used by the PluginFactory */
+  bool init(int argc, char **argv, std::string &error);
+  void done();
+
+  /* Used by the traffic server core while processing requests */
+  TSRemapStatus doRemap(TSHttpTxn rh, TSRemapRequestInfo *rri);
+  void osResponse(TSHttpTxn rh, int os_response_type);
+
+  /* List used by the plugin factory */
+  using self_type  = RemapPluginInst; ///< Self reference type.
+  self_type *_next = nullptr;
+  self_type *_prev = nullptr;
+  using Linkage    = ts::IntrusiveLinkage<self_type>;
+
+  /* Plugin instance = the plugin info + the data returned by the init callback */
+  RemapPluginInfo &_plugin;
+  void *_instance = nullptr;
+};
+
+/**
+ * @brief loads plugins, instantiates and keep track of plugin instances created by this factory.
+ *
+ * - Handles looking through search directories to determine final plugin canonical file name to be used (called here effective
+ * path).
+ * - Makes sure we load each DSO only once per effective path.
+ * - Keeps track of all loaded remap plugins and their instances.
+ * - Maitains the notion of plugin runtime paths and makes sure every factory instance uses different runtime paths for its plugins.
+ * - Makes sure plugin DSOs are loaded for the lifetime of the PluginFactory.
+ *
+ * Each plugin factory instance corresponds to a config reload, each new config file set is meant to use a new factory instance.
+ * A notion of runtime directory is maintained to make sure the DSO library files are not erased or modified while the library are
+ * loaded in memory and make sure if the library file is overriden with a new DSO file that the new overriding plugin's
+ * functionality will be loaded with the next factory, it also handles some problems noticed on different OSes in handling
+ * filesystem links and different dl library implementations.
+ *
+ * @note This is meant to unify the way global and remap plugins are (re)loaded (global plugin support is not implemented yet).
+ */
+class PluginFactory
+{
+  using PluginInstList         = ts::IntrusiveDList<RemapPluginInst::Linkage>;
+  PluginDso::PluginList &_list = PluginDso::_list;
+
+public:
+  PluginFactory();
+  virtual ~PluginFactory();
+
+  PluginFactory &setRuntimeDir(const fs::path &runtimeDir);
+  PluginFactory &addSearchDir(const fs::path &searchDir);
+
+  RemapPluginInst *getRemapPlugin(const fs::path &configPath, int argc, char **argv, std::string &error);
+
+  virtual const char *getUuid();
+  void clean(std::string &error);
+
+  void indicateReload();
+
+protected:
+  PluginDso *findByEffectivePath(const fs::path &path);
+  fs::path getEffectivePath(const fs::path &configPath);
+
+  std::vector<fs::path> _searchDirs; /** @brief ordered list of search paths where we look for plugins */
+  fs::path _runtimeDir;              /** @brief the path where we would create a temporary copies of the plugins to load */
+
+  PluginInstList _instList;
+
+  ATSUuid *_uuid = nullptr;
+  std::error_code _ec;
+  bool _preventiveCleaning = true;
+
+  static constexpr const char *const _tag = "plugin_factory"; /** @brief log tag used by this class */
+};
diff --git a/proxy/http/remap/RemapConfig.cc b/proxy/http/remap/RemapConfig.cc
index 8d5948f..bf33265 100644
--- a/proxy/http/remap/RemapConfig.cc
+++ b/proxy/http/remap/RemapConfig.cc
@@ -32,6 +32,7 @@
 #include "tscore/ink_file.h"
 #include "tscore/Tokenizer.h"
 #include "IPAllow.h"
+#include "PluginFactory.h"
 
 #define modulePrefix "[ReverseProxy]"
 
@@ -708,23 +709,29 @@ remap_check_option(const char **argv, int argc, unsigned long findmode, int *_re
   return ret_flags;
 }
 
-int
+/**
+ * @brief loads a remap plugin
+ *
+ * @pparam mp url mapping
+ * @pparam errbuf error buffer
+ * @pparam errbufsize size of the error buffer
+ * @pparam jump_to_argc
+ * @pparam plugin_found_at
+ * @return success - true, failure - false
+ */
+bool
 remap_load_plugin(const char **argv, int argc, url_mapping *mp, char *errbuf, int errbufsize, int jump_to_argc,
-                  int *plugin_found_at)
+                  int *plugin_found_at, UrlRewrite *rewrite)
 {
-  TSRemapInterface ri;
-  struct stat stat_buf;
-  RemapPluginInfo *pi;
-  char *c, *err, tmpbuf[2048], default_path[PATH_NAME_MAX];
+  char *c, *err;
   const char *new_argv[1024];
-  char *parv[1024];
-  int idx = 0;
-
+  char *pargv[1024];
+  int idx          = 0;
+  int parc         = 0;
   *plugin_found_at = 0;
 
-  memset(parv, 0, sizeof(parv));
+  memset(pargv, 0, sizeof(pargv));
   memset(new_argv, 0, sizeof(new_argv));
-  tmpbuf[0] = 0;
 
   ink_assert((unsigned)argc < countof(new_argv));
 
@@ -737,135 +744,40 @@ remap_load_plugin(const char **argv, int argc, url_mapping *mp, char *errbuf, in
     }
     argv = &new_argv[0];
     if (!remap_check_option(argv, argc, REMAP_OPTFLG_PLUGIN, &idx)) {
-      return -1;
+      return false;
     }
   } else {
     if (unlikely(!mp || (remap_check_option(argv, argc, REMAP_OPTFLG_PLUGIN, &idx) & REMAP_OPTFLG_PLUGIN) == 0)) {
       snprintf(errbuf, errbufsize, "Can't find remap plugin keyword or \"url_mapping\" is nullptr");
-      return -1; /* incorrect input data - almost impossible case */
+      return false; /* incorrect input data - almost impossible case */
     }
   }
 
   if (unlikely((c = (char *)strchr(argv[idx], (int)'=')) == nullptr || !(*(++c)))) {
     snprintf(errbuf, errbufsize, "Can't find remap plugin file name in \"@%s\"", argv[idx]);
-    return -2; /* incorrect input data */
-  }
-
-  if (stat(c, &stat_buf) != 0) {
-    ats_scoped_str plugin_default_path(RecConfigReadPluginDir());
-
-    // Try with the plugin path instead
-    if (strlen(c) + strlen(plugin_default_path) > (PATH_NAME_MAX - 1)) {
-      Debug("remap_plugin", "way too large a path specified for remap plugin");
-      return -3;
-    }
-
-    snprintf(default_path, PATH_NAME_MAX, "%s/%s", static_cast<char *>(plugin_default_path), c);
-    Debug("remap_plugin", "attempting to stat default plugin path: %s", default_path);
-
-    if (stat(default_path, &stat_buf) == 0) {
-      Debug("remap_plugin", "stat successful on %s using that", default_path);
-      c = &default_path[0];
-    } else {
-      snprintf(errbuf, errbufsize, "Can't find remap plugin file \"%s\"", c);
-      return -3;
-    }
+    return false; /* incorrect input data */
   }
 
   Debug("remap_plugin", "using path %s for plugin", c);
 
-  if ((pi = RemapPluginInfo::find_by_path(c)) == nullptr) {
-    pi = new RemapPluginInfo(ts::file::path(c));
-    RemapPluginInfo::add_to_list(pi);
-    Debug("remap_plugin", "New remap plugin info created for \"%s\"", c);
-
-    {
-      uint32_t elevate_access = 0;
-      REC_ReadConfigInteger(elevate_access, "proxy.config.plugin.load_elevated");
-      ElevateAccess access(elevate_access ? ElevateAccess::FILE_PRIVILEGE : 0);
-
-      if ((pi->dl_handle = dlopen(c, RTLD_NOW)) == nullptr) {
-#if defined(freebsd) || defined(openbsd)
-        err = (char *)dlerror();
-#else
-        err = dlerror();
-#endif
-        snprintf(errbuf, errbufsize, "Can't load plugin \"%s\" - %s", c, err ? err : "Unknown dlopen() error");
-        return -4;
-      }
-      pi->init_cb          = reinterpret_cast<RemapPluginInfo::Init_F *>(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_INIT));
-      pi->config_reload_cb = reinterpret_cast<RemapPluginInfo::Reload_F *>(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_CONFIG_RELOAD));
-      pi->done_cb          = reinterpret_cast<RemapPluginInfo::Done_F *>(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_DONE));
-      pi->new_instance_cb =
-        reinterpret_cast<RemapPluginInfo::New_Instance_F *>(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_NEW_INSTANCE));
-      pi->delete_instance_cb =
-        reinterpret_cast<RemapPluginInfo::Delete_Instance_F *>(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_DELETE_INSTANCE));
-      pi->do_remap_cb    = reinterpret_cast<RemapPluginInfo::Do_Remap_F *>(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_DO_REMAP));
-      pi->os_response_cb = reinterpret_cast<RemapPluginInfo::OS_Response_F *>(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_OS_RESPONSE));
-
-      int retcode = 0;
-      if (!pi->init_cb) {
-        snprintf(errbuf, errbufsize, R"(Can't find "%s" function in remap plugin "%s")", TSREMAP_FUNCNAME_INIT, c);
-        retcode = -10;
-      } else if (!pi->new_instance_cb && pi->delete_instance_cb) {
-        snprintf(errbuf, errbufsize,
-                 R"(Can't find "%s" function in remap plugin "%s" which is required if "%s" function exists)",
-                 TSREMAP_FUNCNAME_NEW_INSTANCE, c, TSREMAP_FUNCNAME_DELETE_INSTANCE);
-        retcode = -11;
-      } else if (!pi->do_remap_cb) {
-        snprintf(errbuf, errbufsize, R"(Can't find "%s" function in remap plugin "%s")", TSREMAP_FUNCNAME_DO_REMAP, c);
-        retcode = -12;
-      } else if (pi->new_instance_cb && !pi->delete_instance_cb) {
-        snprintf(errbuf, errbufsize,
-                 R"(Can't find "%s" function in remap plugin "%s" which is required if "%s" function exists)",
-                 TSREMAP_FUNCNAME_DELETE_INSTANCE, c, TSREMAP_FUNCNAME_NEW_INSTANCE);
-        retcode = -13;
-      }
-      if (retcode) {
-        if (errbuf && errbufsize > 0) {
-          Debug("remap_plugin", "%s", errbuf);
-        }
-        dlclose(pi->dl_handle);
-        pi->dl_handle = nullptr;
-        return retcode;
-      }
-      memset(&ri, 0, sizeof(ri));
-      ri.size            = sizeof(ri);
-      ri.tsremap_version = TSREMAP_VERSION;
-
-      if (pi->init_cb(&ri, tmpbuf, sizeof(tmpbuf) - 1) != TS_SUCCESS) {
-        snprintf(errbuf, errbufsize, "Failed to initialize plugin \"%s\": %s", pi->path.c_str(),
-                 tmpbuf[0] ? tmpbuf : "Unknown plugin error");
-        return -5;
-      }
-    } // done elevating access
-    Debug("remap_plugin", "Remap plugin \"%s\" - initialization completed", c);
-  }
-
-  if (!pi->dl_handle) {
-    snprintf(errbuf, errbufsize, "Can't load plugin \"%s\"", c);
-    return -6;
-  }
-
+  /* Prepare remap plugin parameters from the config */
   if ((err = mp->fromURL.string_get(nullptr)) == nullptr) {
     snprintf(errbuf, errbufsize, "Can't load fromURL from URL class");
-    return -7;
+    return false;
   }
-
-  int parc     = 0;
-  parv[parc++] = ats_strdup(err);
+  pargv[parc++] = ats_strdup(err);
   ats_free(err);
 
   if ((err = mp->toURL.string_get(nullptr)) == nullptr) {
     snprintf(errbuf, errbufsize, "Can't load toURL from URL class");
-    return -7;
+    return false;
   }
-  parv[parc++] = ats_strdup(err);
+  pargv[parc++] = ats_strdup(err);
   ats_free(err);
 
   bool plugin_encountered = false;
   // how many plugin parameters we have for this remapping
-  for (idx = 0; idx < argc && parc < (int)(countof(parv) - 1); idx++) {
+  for (idx = 0; idx < argc && parc < static_cast<int>(countof(pargv) - 1); idx++) {
     if (plugin_encountered && !strncasecmp("plugin=", argv[idx], 7) && argv[idx][7]) {
       *plugin_found_at = idx;
       break; // if there is another plugin, lets deal with that later
@@ -876,7 +788,7 @@ remap_load_plugin(const char **argv, int argc, url_mapping *mp, char *errbuf, in
     }
 
     if (!strncasecmp("pparam=", argv[idx], 7) && argv[idx][7]) {
-      parv[parc++] = const_cast<char *>(&(argv[idx][7]));
+      pargv[parc++] = const_cast<char *>(&(argv[idx][7]));
     }
   }
 
@@ -885,43 +797,32 @@ remap_load_plugin(const char **argv, int argc, url_mapping *mp, char *errbuf, in
     Debug("url_rewrite", "Argument %d: %s", k, argv[k]);
   }
 
-  Debug("url_rewrite", "Viewing parsed plugin parameters for %s: [%d]", pi->path.c_str(), *plugin_found_at);
+  Debug("url_rewrite", "Viewing parsed plugin parameters for %s: [%d]", c, *plugin_found_at);
   for (int k = 0; k < parc; k++) {
-    Debug("url_rewrite", "Argument %d: %s", k, parv[k]);
+    Debug("url_rewrite", "Argument %d: %s", k, pargv[k]);
   }
 
-  Debug("remap_plugin", "creating new plugin instance");
-
-  void *ih         = nullptr;
-  TSReturnCode res = TS_SUCCESS;
-  if (pi->new_instance_cb) {
-#if (!defined(kfreebsd) && defined(freebsd)) || defined(darwin)
-    optreset = 1;
-#endif
-#if defined(__GLIBC__)
-    optind = 0;
-#else
-    optind = 1;
-#endif
-    opterr = 0;
-    optarg = nullptr;
-
-    res = pi->new_instance_cb(parc, parv, &ih, tmpbuf, sizeof(tmpbuf) - 1);
-  }
-
-  Debug("remap_plugin", "done creating new plugin instance");
+  RemapPluginInst *pi = nullptr;
+  std::string error;
+  {
+    uint32_t elevate_access = 0;
+    REC_ReadConfigInteger(elevate_access, "proxy.config.plugin.load_elevated");
+    ElevateAccess access(elevate_access ? ElevateAccess::FILE_PRIVILEGE : 0);
 
-  ats_free(parv[0]); // fromURL
-  ats_free(parv[1]); // toURL
+    pi = rewrite->pluginFactory.getRemapPlugin(ts::file::path(const_cast<const char *>(c)), parc, pargv, error);
+  } // done elevating access
 
-  if (res != TS_SUCCESS) {
-    snprintf(errbuf, errbufsize, "Failed to create instance for plugin \"%s\": %s", c, tmpbuf[0] ? tmpbuf : "Unknown plugin error");
-    return -8;
+  bool result = true;
+  if (nullptr == pi) {
+    snprintf(errbuf, errbufsize, "%s", error.c_str());
+  } else {
+    mp->add_plugin_instance(pi);
   }
 
-  mp->add_plugin(pi, ih);
+  ats_free(pargv[0]); // fromURL
+  ats_free(pargv[1]); // toURL
 
-  return 0;
+  return result;
 }
 /** will process the regex mapping configuration and create objects in
     output argument reg_map. It assumes existing data in reg_map is
@@ -1368,8 +1269,8 @@ remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti)
         int jump_to_argc    = 0;
 
         // this loads the first plugin
-        if (remap_load_plugin((const char **)bti->argv, bti->argc, new_mapping, errStrBuf, sizeof(errStrBuf), 0,
-                              &plugin_found_at)) {
+        if (!remap_load_plugin((const char **)bti->argv, bti->argc, new_mapping, errStrBuf, sizeof(errStrBuf), 0, &plugin_found_at,
+                               bti->rewrite)) {
           Debug("remap_plugin", "Remap plugin load error - %s", errStrBuf[0] ? errStrBuf : "Unknown error");
           errStr = errStrBuf;
           goto MAP_ERROR;
@@ -1377,8 +1278,8 @@ remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti)
         // this loads any subsequent plugins (if present)
         while (plugin_found_at) {
           jump_to_argc += plugin_found_at;
-          if (remap_load_plugin((const char **)bti->argv, bti->argc, new_mapping, errStrBuf, sizeof(errStrBuf), jump_to_argc,
-                                &plugin_found_at)) {
+          if (!remap_load_plugin((const char **)bti->argv, bti->argc, new_mapping, errStrBuf, sizeof(errStrBuf), jump_to_argc,
+                                 &plugin_found_at, bti->rewrite)) {
             Debug("remap_plugin", "Remap plugin load error - %s", errStrBuf[0] ? errStrBuf : "Unknown error");
             errStr = errStrBuf;
             goto MAP_ERROR;
@@ -1421,7 +1322,7 @@ remap_parse_config(const char *path, UrlRewrite *rewrite)
 
   // If this happens to be a config reload, the list of loaded remap plugins is non-empty, and we
   // can signal all these plugins that a reload has begun.
-  RemapPluginInfo::indicate_reload();
+  rewrite->pluginFactory.indicateReload();
   bti.rewrite = rewrite;
   return remap_parse_config_bti(path, &bti);
 }
diff --git a/proxy/http/remap/RemapPluginInfo.cc b/proxy/http/remap/RemapPluginInfo.cc
index db6dfe5..8a1c00e 100644
--- a/proxy/http/remap/RemapPluginInfo.cc
+++ b/proxy/http/remap/RemapPluginInfo.cc
@@ -19,62 +19,252 @@
   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 <unistd.h>
+
 #include "RemapPluginInfo.h"
 #include "tscore/ink_string.h"
 #include "tscore/ink_memory.h"
+#include "tscore/ink_apidefs.h"
+
+#include "RemapPluginInfo.h"
+#ifdef PLUGIN_DSO_TESTS
+#include "unit-tests/plugin_testing_common.h"
+#else
+#include "tscore/Diags.h"
+#endif
+
+/**
+ * @brief helper function that returns the function address from the plugin DSO
+ *
+ * There can be valid defined DSO symbols that are NULL
+ * but when it comes to functions we can assume that
+ * if not defined we can return nullptr and a valid address if the are defined.
+ * @param symbol function symbol name
+ * @param error error messages in case of symbol is not found
+ * @return function address or nullptr if not found.
+ */
+template <class T>
+T *
+RemapPluginInfo::getFunctionSymbol(const char *symbol)
+{
+  std::string error; /* ignore the error, return nullptr if symbol not defined */
+  void *address = nullptr;
+  getSymbol(symbol, address, error);
+  return reinterpret_cast<T *>(address);
+}
+
+std::string
+RemapPluginInfo::missingRequiredSymbolError(const std::string &pluginName, const char *required, const char *requiring)
+{
+  std::string error;
+  error.assign("plugin ").append(pluginName).append(" missing required function ").append(required);
+  if (requiring) {
+    error.append(" if ").append(requiring).append(" is defined");
+  }
+  return error;
+}
+
+RemapPluginInfo::RemapPluginInfo(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath)
+  : PluginDso(configPath, effectivePath, runtimePath)
+{
+}
+
+bool
+RemapPluginInfo::load(std::string &error)
+{
+  error.clear();
+
+  if (!PluginDso::load(error)) {
+    return false;
+  }
+
+  init_cb            = getFunctionSymbol<Init_F>(TSREMAP_FUNCNAME_INIT);
+  config_reload_cb   = getFunctionSymbol<Reload_F>(TSREMAP_FUNCNAME_CONFIG_RELOAD);
+  done_cb            = getFunctionSymbol<Done_F>(TSREMAP_FUNCNAME_DONE);
+  new_instance_cb    = getFunctionSymbol<New_Instance_F>(TSREMAP_FUNCNAME_NEW_INSTANCE);
+  delete_instance_cb = getFunctionSymbol<Delete_Instance_F>(TSREMAP_FUNCNAME_DELETE_INSTANCE);
+  do_remap_cb        = getFunctionSymbol<Do_Remap_F>(TSREMAP_FUNCNAME_DO_REMAP);
+  os_response_cb     = getFunctionSymbol<OS_Response_F>(TSREMAP_FUNCNAME_OS_RESPONSE);
+
+  /* Validate if the callback TSREMAP functions are specified correctly in the plugin. */
+  bool valid = true;
+  if (!init_cb) {
+    error = missingRequiredSymbolError(_configPath.string(), TSREMAP_FUNCNAME_INIT);
+    valid = false;
+  } else if (!do_remap_cb) {
+    error = missingRequiredSymbolError(_configPath.string(), TSREMAP_FUNCNAME_DO_REMAP);
+    valid = false;
+  } else if (!new_instance_cb && delete_instance_cb) {
+    error = missingRequiredSymbolError(_configPath.string(), TSREMAP_FUNCNAME_NEW_INSTANCE, TSREMAP_FUNCNAME_DELETE_INSTANCE);
+    valid = false;
+  } else if (new_instance_cb && !delete_instance_cb) {
+    error = missingRequiredSymbolError(_configPath.string(), TSREMAP_FUNCNAME_DELETE_INSTANCE, TSREMAP_FUNCNAME_NEW_INSTANCE);
+    valid = false;
+  }
+
+  if (valid) {
+    Debug(_tag, "plugin '%s' callbacks validated", _configPath.c_str());
+  } else {
+    Error("plugin '%s' callbacks validation failed: %s", _configPath.c_str(), error.c_str());
+  }
+  return valid;
+}
+
+/* Initialize plugin (required). */
+bool
+RemapPluginInfo::init(std::string &error)
+{
+  TSRemapInterface ri;
+  bool result = true;
 
-RemapPluginInfo::List RemapPluginInfo::g_list;
+  Debug(_tag, "started initializing plugin '%s'", _configPath.c_str());
 
-RemapPluginInfo::RemapPluginInfo(ts::file::path &&library_path) : path(std::move(library_path)) {}
+  /* A buffer to get the error from the plugin instance init function, be defensive here. */
+  char tmpbuf[2048];
+  ink_zero(tmpbuf);
 
-RemapPluginInfo::~RemapPluginInfo()
+  ink_zero(ri);
+  ri.size            = sizeof(ri);
+  ri.tsremap_version = TSREMAP_VERSION;
+
+  setPluginContext();
+
+  if (init_cb && init_cb(&ri, tmpbuf, sizeof(tmpbuf) - 1) != TS_SUCCESS) {
+    error.assign("failed to initialize plugin ")
+      .append(_configPath.string())
+      .append(": ")
+      .append(tmpbuf[0] ? tmpbuf : "Unknown plugin error");
+    result = false;
+  }
+
+  resetPluginContext();
+
+  Debug(_tag, "finished initializing plugin '%s'", _configPath.c_str());
+
+  return result;
+}
+
+/* Called when plugin is unloaded (optional). */
+void
+RemapPluginInfo::done()
 {
-  if (dl_handle) {
-    dlclose(dl_handle);
+  if (done_cb) {
+    done_cb();
   }
 }
 
-//
-// Find a plugin by path from our linked list
-//
-RemapPluginInfo *
-RemapPluginInfo::find_by_path(std::string_view library_path)
+bool
+RemapPluginInfo::initInstance(int argc, char **argv, void **ih, std::string &error)
 {
-  auto spot = std::find_if(g_list.begin(), g_list.end(),
-                           [&](self_type const &info) -> bool { return 0 == library_path.compare(info.path.view()); });
-  return spot == g_list.end() ? nullptr : static_cast<self_type *>(spot);
+  TSReturnCode res = TS_SUCCESS;
+  bool result      = true;
+
+  Debug(_tag, "started initializing instance of plugin '%s'", _configPath.c_str());
+
+  /* A buffer to get the error from the plugin instance init function, be defensive here. */
+  char tmpbuf[2048];
+  ink_zero(tmpbuf);
+
+  if (new_instance_cb) {
+#if defined(freebsd) || defined(darwin)
+    optreset = 1;
+#endif
+#if defined(__GLIBC__)
+    optind = 0;
+#else
+    optind = 1;
+#endif
+    opterr = 0;
+    optarg = nullptr;
+
+    setPluginContext();
+
+    res = new_instance_cb(argc, argv, ih, tmpbuf, sizeof(tmpbuf) - 1);
+
+    resetPluginContext();
+
+    if (TS_SUCCESS != res) {
+      error.assign("failed to create instance for plugin ")
+        .append(_configPath.string())
+        .append(": ")
+        .append(tmpbuf[0] ? tmpbuf : "Unknown plugin error");
+      result = false;
+    }
+  }
+
+  Debug(_tag, "finished initializing instance of plugin '%s'", _configPath.c_str());
+
+  return result;
 }
 
-//
-// Add a plugin to the linked list
-//
 void
-RemapPluginInfo::add_to_list(RemapPluginInfo *pi)
+RemapPluginInfo::doneInstance(void *ih)
 {
-  g_list.append(pi);
+  setPluginContext();
+
+  if (delete_instance_cb) {
+    delete_instance_cb(ih);
+  }
+
+  resetPluginContext();
+}
+
+TSRemapStatus
+RemapPluginInfo::doRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+  TSRemapStatus result = TSREMAP_NO_REMAP;
+
+  setPluginContext();
+
+  if (do_remap_cb) {
+    result = do_remap_cb(ih, rh, rri);
+  }
+
+  resetPluginContext();
+
+  return result;
 }
 
-//
-// Remove and delete all plugins from a list.
-//
 void
-RemapPluginInfo::delete_list()
+RemapPluginInfo::osResponse(void *ih, TSHttpTxn rh, int os_response_type)
 {
-  g_list.apply([](self_type *info) -> void { delete info; });
-  g_list.clear();
+  setPluginContext();
+
+  if (os_response_cb) {
+    os_response_cb(ih, rh, os_response_type);
+  }
+
+  resetPluginContext();
 }
 
-//
-// Tell all plugins (that so wish) that remap.config is being reloaded
-//
+RemapPluginInfo::~RemapPluginInfo() {}
+
 void
-RemapPluginInfo::indicate_reload()
+RemapPluginInfo::indicateReload()
 {
-  g_list.apply([](self_type *info) -> void {
-    if (info->config_reload_cb) {
-      info->config_reload_cb();
-    }
-  });
+  setPluginContext();
+
+  if (config_reload_cb) {
+    config_reload_cb();
+  }
+
+  resetPluginContext();
+}
+
+inline void
+RemapPluginInfo::setPluginContext()
+{
+  _tempContext        = pluginThreadContext;
+  pluginThreadContext = this;
+  Debug(_tag, "change plugin context from dso-addr:%p to dso-addr:%p", pluginThreadContext, _tempContext);
+}
+
+inline void
+RemapPluginInfo::resetPluginContext()
+{
+  Debug(_tag, "change plugin context from dso-addr:%p to dso-addr:%p (restore)", this, pluginThreadContext);
+  pluginThreadContext = _tempContext;
 }
diff --git a/proxy/http/remap/RemapPluginInfo.h b/proxy/http/remap/RemapPluginInfo.h
index 7802b5c..cc5941d 100644
--- a/proxy/http/remap/RemapPluginInfo.h
+++ b/proxy/http/remap/RemapPluginInfo.h
@@ -19,14 +19,22 @@
   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.
+
  */
 
 #pragma once
+
+#include <string>
+#include <tuple>
+
 #include "tscore/ink_platform.h"
-#include "tscpp/util/IntrusiveDList.h"
-#include "tscore/ts_file.h"
 #include "ts/apidefs.h"
 #include "ts/remap.h"
+#include "PluginDso.h"
+
+class url_mapping;
+
+extern thread_local PluginThreadContext *pluginThreadContext;
 
 static constexpr const char *const TSREMAP_FUNCNAME_INIT            = "TSRemapInit";
 static constexpr const char *const TSREMAP_FUNCNAME_CONFIG_RELOAD   = "TSRemapConfigReload";
@@ -36,14 +44,13 @@ static constexpr const char *const TSREMAP_FUNCNAME_DELETE_INSTANCE = "TSRemapDe
 static constexpr const char *const TSREMAP_FUNCNAME_DO_REMAP        = "TSRemapDoRemap";
 static constexpr const char *const TSREMAP_FUNCNAME_OS_RESPONSE     = "TSRemapOSResponse";
 
-/** Information for a remap plugin.
- *  This stores the name of the library file and the callback entry points.
+/**
+ * Holds information for a remap plugin, remap specific callback entry points for plugin init/done and instance init/done, do_remap,
+ * origin server response,
  */
-class RemapPluginInfo
+class RemapPluginInfo : public PluginDso
 {
 public:
-  using self_type = RemapPluginInfo; ///< Self reference type.
-
   /// Initialization function, called on library load.
   using Init_F = TSReturnCode(TSRemapInterface *api_info, char *errbuf, int errbuf_size);
   /// Reload function, called to inform the plugin of a configuration reload.
@@ -59,12 +66,6 @@ public:
   /// I have no idea what this is for.
   using OS_Response_F = void(void *ih, TSHttpTxn rh, int os_response_type);
 
-  self_type *_next = nullptr;
-  self_type *_prev = nullptr;
-  using Linkage    = ts::IntrusiveLinkage<self_type>;
-  using List       = ts::IntrusiveDList<Linkage>;
-
-  ts::file::path path;
   void *dl_handle                       = nullptr; /* "handle" for the dynamic library */
   Init_F *init_cb                       = nullptr;
   Reload_F *config_reload_cb            = nullptr;
@@ -74,16 +75,37 @@ public:
   Do_Remap_F *do_remap_cb               = nullptr;
   OS_Response_F *os_response_cb         = nullptr;
 
-  explicit RemapPluginInfo(ts::file::path &&library_path);
+  RemapPluginInfo(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath);
   ~RemapPluginInfo();
 
-  static self_type *find_by_path(std::string_view library_path);
-  static void add_to_list(self_type *pi);
-  static void delete_list();
-  static void indicate_reload();
+  /* Overload to add / execute remap plugin specific tasks during the plugin loading */
+  virtual bool load(std::string &error);
+
+  /* Used by the factory to invoke callbacks during plugin load, init and unload  */
+  virtual bool init(std::string &error);
+  virtual void done(void);
+
+  /* Used by the facility that handles remap plugin instances to invoke callbacks per plugin instance */
+  bool initInstance(int argc, char **argv, void **ih, std::string &error);
+  void doneInstance(void *ih);
+
+  /* Used by the other parts of the traffic server core while handling requests */
+  TSRemapStatus doRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri);
+  void osResponse(void *ih, TSHttpTxn rh, int os_response_type);
+
+  /* Used by traffic server core to indicate configuration reload */
+  virtual void indicateReload();
+
+protected:
+  /* Utility to be used only with unit testing */
+  std::string missingRequiredSymbolError(const std::string &pluginName, const char *required, const char *requiring = nullptr);
+  template <class T> T *getFunctionSymbol(const char *symbol);
+  void setPluginContext();
+  void resetPluginContext();
+
+  static constexpr const char *const _tag = "plugin_remap"; /** @brief log tag used by this class */
 
-  /// Singleton list of remap plugin info instances.
-  static List g_list;
+  PluginThreadContext *_tempContext = nullptr;
 };
 
 /**
diff --git a/proxy/http/remap/RemapPlugins.cc b/proxy/http/remap/RemapPlugins.cc
index 6a66445..3e69c43 100644
--- a/proxy/http/remap/RemapPlugins.cc
+++ b/proxy/http/remap/RemapPlugins.cc
@@ -19,6 +19,7 @@
   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 "RemapPlugins.h"
@@ -26,16 +27,14 @@
 ClassAllocator<RemapPlugins> pluginAllocator("RemapPluginsAlloc");
 
 TSRemapStatus
-RemapPlugins::run_plugin(RemapPluginInfo *plugin)
+RemapPlugins::run_plugin(RemapPluginInst *plugin)
 {
   ink_assert(_s);
 
   TSRemapStatus plugin_retcode;
   TSRemapRequestInfo rri;
-  url_mapping *map = _s->url_map.getMapping();
-  URL *map_from    = _s->url_map.getFromURL();
-  URL *map_to      = _s->url_map.getToURL();
-  void *ih         = map->get_instance(_cur);
+  URL *map_from = _s->url_map.getFromURL();
+  URL *map_to   = _s->url_map.getToURL();
 
   // This is the equivalent of TSHttpTxnClientReqGet(), which every remap plugin would
   // have to call.
@@ -51,11 +50,10 @@ RemapPlugins::run_plugin(RemapPluginInfo *plugin)
 
   // Prepare State for the future
   if (_cur == 0) {
-    _s->fp_tsremap_os_response = plugin->os_response_cb;
-    _s->remap_plugin_instance  = ih;
+    _s->os_response_plugin_inst = plugin;
   }
 
-  plugin_retcode = plugin->do_remap_cb(ih, reinterpret_cast<TSHttpTxn>(_s->state_machine), &rri);
+  plugin_retcode = plugin->doRemap(reinterpret_cast<TSHttpTxn>(_s->state_machine), &rri);
   // TODO: Deal with negative return codes here
   if (plugin_retcode < 0) {
     plugin_retcode = TSREMAP_NO_REMAP;
@@ -82,7 +80,7 @@ bool
 RemapPlugins::run_single_remap()
 {
   url_mapping *map             = _s->url_map.getMapping();
-  RemapPluginInfo *plugin      = map->get_plugin(_cur); // get the nth plugin in our list of plugins
+  RemapPluginInst *plugin      = map->get_plugin_instance(_cur); // get the nth plugin in our list of plugins
   TSRemapStatus plugin_retcode = TSREMAP_NO_REMAP;
   bool zret                    = true; // default - last iteration.
   Debug("url_rewrite", "running single remap rule id %d for the %d%s time", map->map_id, _cur,
@@ -109,7 +107,7 @@ RemapPlugins::run_single_remap()
 
     if (TSREMAP_NO_REMAP_STOP == plugin_retcode || TSREMAP_DID_REMAP_STOP == plugin_retcode) {
       Debug("url_rewrite", "breaking remap plugin chain since last plugin said we should stop after %d rewrites", _rewritten);
-    } else if (_cur >= map->plugin_count()) {
+    } else if (_cur >= map->plugin_instance_count()) {
       Debug("url_rewrite", "completed all remap plugins for rule id %d, changed by %d plugins", map->map_id, _rewritten);
     } else {
       Debug("url_rewrite", "completed single remap, attempting another via immediate callback");
diff --git a/proxy/http/remap/RemapPlugins.h b/proxy/http/remap/RemapPlugins.h
index 421b55c..1bec1b9 100644
--- a/proxy/http/remap/RemapPlugins.h
+++ b/proxy/http/remap/RemapPlugins.h
@@ -19,11 +19,8 @@
   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.
- */
 
-/**
- * Remap plugins class
- **/
+ */
 
 #pragma once
 
@@ -68,7 +65,7 @@ struct RemapPlugins : public Continuation {
 
   int run_remap(int event, Event *e);
   bool run_single_remap();
-  TSRemapStatus run_plugin(RemapPluginInfo *plugin);
+  TSRemapStatus run_plugin(RemapPluginInst *plugin);
 
   Action action;
 
diff --git a/proxy/http/remap/UrlMapping.cc b/proxy/http/remap/UrlMapping.cc
index 782b3e2..61a375d 100644
--- a/proxy/http/remap/UrlMapping.cc
+++ b/proxy/http/remap/UrlMapping.cc
@@ -19,6 +19,7 @@
   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 "tscore/ink_defs.h"
@@ -30,50 +31,25 @@
  *
  **/
 bool
-url_mapping::add_plugin(RemapPluginInfo *i, void *ih)
+url_mapping::add_plugin_instance(RemapPluginInst *i)
 {
-  _plugin_list.push_back(i);
-  _instance_data.push_back(ih);
-
+  _plugin_inst_list.push_back(i);
   return true;
 }
 
 /**
  *
  **/
-RemapPluginInfo *
-url_mapping::get_plugin(std::size_t index) const
+RemapPluginInst *
+url_mapping::get_plugin_instance(std::size_t index) const
 {
-  Debug("url_rewrite", "get_plugin says we have %zu plugins and asking for plugin %zu", plugin_count(), index);
-  if (index < _plugin_list.size()) {
-    return _plugin_list[index];
+  Debug("url_rewrite", "get_plugin says we have %zu plugins and asking for plugin %zu", _plugin_inst_list.size(), index);
+  if (index < _plugin_inst_list.size()) {
+    return _plugin_inst_list[index];
   }
   return nullptr;
 }
 
-void *
-url_mapping::get_instance(std::size_t index) const
-{
-  if (index < _instance_data.size()) {
-    return _instance_data[index];
-  }
-  return nullptr;
-}
-
-/**
- *
- **/
-void
-url_mapping::delete_instance(unsigned int index)
-{
-  void *ih           = get_instance(index);
-  RemapPluginInfo *p = get_plugin(index);
-
-  if (ih && p && p->delete_instance_cb) {
-    p->delete_instance_cb(ih);
-  }
-}
-
 /**
  *
  **/
@@ -96,13 +72,6 @@ url_mapping::~url_mapping()
     delete rc;
   }
 
-  // Delete all instance data, this gets ugly because to delete the instance data, we also
-  // must know which plugin this is associated with. Hence, looping with index instead of a
-  // normal iterator. ToDo: Maybe we can combine them into another container.
-  for (std::size_t i = 0; i < plugin_count(); ++i) {
-    delete_instance(i);
-  }
-
   // Delete filters
   while ((afr = filter) != nullptr) {
     filter = afr->next;
@@ -122,7 +91,8 @@ url_mapping::Print()
   fromURL.string_get_buf(from_url_buf, (int)sizeof(from_url_buf));
   toURL.string_get_buf(to_url_buf, (int)sizeof(to_url_buf));
   printf("\t %s %s=> %s %s <%s> [plugins %s enabled; running with %zu plugins]\n", from_url_buf, unique ? "(unique)" : "",
-         to_url_buf, homePageRedirect ? "(R)" : "", tag ? tag : "", plugin_count() > 0 ? "are" : "not", plugin_count());
+         to_url_buf, homePageRedirect ? "(R)" : "", tag ? tag : "", _plugin_inst_list.size() > 0 ? "are" : "not",
+         _plugin_inst_list.size());
 }
 
 /**
diff --git a/proxy/http/remap/UrlMapping.h b/proxy/http/remap/UrlMapping.h
index 1b55e52..c6de768 100644
--- a/proxy/http/remap/UrlMapping.h
+++ b/proxy/http/remap/UrlMapping.h
@@ -19,6 +19,7 @@
   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.
+
  */
 
 #pragma once
@@ -29,6 +30,7 @@
 #include "AclFiltering.h"
 #include "URL.h"
 #include "RemapPluginInfo.h"
+#include "PluginFactory.h"
 #include "tscore/Regex.h"
 #include "tscore/List.h"
 
@@ -79,17 +81,15 @@ class url_mapping
 public:
   ~url_mapping();
 
-  bool add_plugin(RemapPluginInfo *i, void *ih);
-  RemapPluginInfo *get_plugin(std::size_t) const;
-  void *get_instance(std::size_t) const;
+  bool add_plugin_instance(RemapPluginInst *i);
+  RemapPluginInst *get_plugin_instance(std::size_t) const;
 
   std::size_t
-  plugin_count() const
+  plugin_instance_count() const
   {
-    return _plugin_list.size();
+    return _plugin_inst_list.size();
   }
 
-  void delete_instance(unsigned int index);
   void Print();
 
   int from_path_len = 0;
@@ -122,8 +122,7 @@ public:
   };
 
 private:
-  std::vector<RemapPluginInfo *> _plugin_list;
-  std::vector<void *> _instance_data;
+  std::vector<RemapPluginInst *> _plugin_inst_list;
   int _rank = 0;
 };
 
diff --git a/proxy/http/remap/UrlRewrite.cc b/proxy/http/remap/UrlRewrite.cc
index 6f2e83d..91aa3e8 100644
--- a/proxy/http/remap/UrlRewrite.cc
+++ b/proxy/http/remap/UrlRewrite.cc
@@ -19,6 +19,7 @@
   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 "UrlRewrite.h"
@@ -79,6 +80,9 @@ UrlRewrite::load()
 
   REC_ReadConfigInteger(reverse_proxy, "proxy.config.reverse_proxy.enabled");
 
+  /* Initialize the plugin factory */
+  pluginFactory.setRuntimeDir(RecConfigReadRuntimeDir()).addSearchDir(RecConfigReadPluginDir());
+
   if (0 == this->BuildTable(config_file_path)) {
     _valid = true;
     if (is_debug_tag_set("url_rewrite")) {
diff --git a/proxy/http/remap/UrlRewrite.h b/proxy/http/remap/UrlRewrite.h
index 89661ca..84386ee 100644
--- a/proxy/http/remap/UrlRewrite.h
+++ b/proxy/http/remap/UrlRewrite.h
@@ -19,6 +19,7 @@
   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.
+
  */
 
 #pragma once
@@ -28,6 +29,7 @@
 #include "UrlMappingPathIndex.h"
 #include "HttpTransact.h"
 #include "tscore/Regex.h"
+#include "PluginFactory.h"
 
 #include <memory>
 
@@ -208,6 +210,8 @@ public:
   int num_rules_redirect_temporary     = 0;
   int num_rules_forward_with_recv_port = 0;
 
+  PluginFactory pluginFactory;
+
 private:
   bool _valid = false;
 
diff --git a/proxy/http/remap/unit-tests/plugin_misc_cb.cc b/proxy/http/remap/unit-tests/plugin_misc_cb.cc
new file mode 100644
index 0000000..f0792fc
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_misc_cb.cc
@@ -0,0 +1,106 @@
+/** @file
+
+  A test plugin for testing Plugin's Dynamic Shared Objects (DSO)
+
+  @section license License
+
+  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.
+
+  @section details Details
+
+  Implements code necessary for Reverse Proxy which mostly consists of
+  general purpose hostname substitution in URLs.
+
+ */
+
+#include "plugin_testing_common.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+
+PluginDebugObject debugObject;
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+  debugObject.contextInit = pluginThreadContext;
+  return TS_SUCCESS;
+}
+
+void
+TSRemapDone(void)
+{
+}
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+  return TSREMAP_NO_REMAP;
+}
+
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_size)
+{
+  debugObject.contextInitInstance = pluginThreadContext;
+
+  return TS_SUCCESS;
+}
+
+void
+TSRemapDeleteInstance(void *)
+{
+}
+
+void
+TSRemapOSResponse(void *ih, TSHttpTxn rh, int os_response_type)
+{
+}
+
+void
+TSPluginInit(int argc, const char *argv[])
+{
+}
+
+void
+TSRemapConfigReload(void)
+{
+}
+
+/* This is meant for test with plugins of different versions */
+int
+pluginDsoVersionTest()
+{
+#ifdef PLUGINDSOVER
+  return PLUGINDSOVER;
+#else
+  return -1;
+#endif
+}
+
+void *
+getPluginDebugObjectTest()
+{
+  return (void *)&debugObject;
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/plugin_missing_deleteinstance.cc b/proxy/http/remap/unit-tests/plugin_missing_deleteinstance.cc
new file mode 100644
index 0000000..03ded2d
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_missing_deleteinstance.cc
@@ -0,0 +1,57 @@
+/** @file
+
+  A test plugin for testing Plugin's Dynamic Shared Objects (DSO)
+
+  @section license License
+
+  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.
+
+  @section details Details
+
+  Implements code necessary for Reverse Proxy which mostly consists of
+  general purpose hostname substitution in URLs.
+
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+  return TS_SUCCESS;
+}
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+  return TSREMAP_NO_REMAP;
+}
+
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_size)
+{
+  return TS_SUCCESS;
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/plugin_missing_doremap.cc b/proxy/http/remap/unit-tests/plugin_missing_doremap.cc
new file mode 100644
index 0000000..f727f6d
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_missing_doremap.cc
@@ -0,0 +1,45 @@
+/** @file
+
+  A test plugin for testing Plugin's Dynamic Shared Objects (DSO)
+
+  @section license License
+
+  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.
+
+  @section details Details
+
+  Implements code necessary for Reverse Proxy which mostly consists of
+  general purpose hostname substitution in URLs.
+
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+  return TS_SUCCESS;
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/plugin_missing_init.cc b/proxy/http/remap/unit-tests/plugin_missing_init.cc
new file mode 100644
index 0000000..265bfa5
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_missing_init.cc
@@ -0,0 +1,45 @@
+/** @file
+
+  A test plugin for testing Plugin's Dynamic Shared Objects (DSO)
+
+  @section license License
+
+  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.
+
+  @section details Details
+
+  Implements code necessary for Reverse Proxy which mostly consists of
+  general purpose hostname substitution in URLs.
+
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+  return TSREMAP_NO_REMAP;
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/plugin_missing_newinstance.cc b/proxy/http/remap/unit-tests/plugin_missing_newinstance.cc
new file mode 100644
index 0000000..bed55d6
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_missing_newinstance.cc
@@ -0,0 +1,56 @@
+/** @file
+
+  A test plugin for testing Plugin's Dynamic Shared Objects (DSO)
+
+  @section license License
+
+  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.
+
+  @section details Details
+
+  Implements code necessary for Reverse Proxy which mostly consists of
+  general purpose hostname substitution in URLs.
+
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+  return TS_SUCCESS;
+}
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+  return TSREMAP_NO_REMAP;
+}
+
+void
+TSRemapDeleteInstance(void *)
+{
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/plugin_required_cb.cc b/proxy/http/remap/unit-tests/plugin_required_cb.cc
new file mode 100644
index 0000000..65b8026
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_required_cb.cc
@@ -0,0 +1,51 @@
+/** @file
+
+  A test plugin for testing Plugin's Dynamic Shared Objects (DSO)
+
+  @section license License
+
+  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.
+
+  @section details Details
+
+  Implements code necessary for Reverse Proxy which mostly consists of
+  general purpose hostname substitution in URLs.
+
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+  return TS_SUCCESS;
+}
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+  return TSREMAP_NO_REMAP;
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/plugin_testing_calls.cc b/proxy/http/remap/unit-tests/plugin_testing_calls.cc
new file mode 100644
index 0000000..89c8df2
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_testing_calls.cc
@@ -0,0 +1,130 @@
+/** @file
+
+  A test plugin for testing Plugin's Dynamic Shared Objects (DSO)
+
+  @section license License
+
+  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.
+
+  @section details Details
+
+  Implements code necessary for Reverse Proxy which mostly consists of
+  general purpose hostname substitution in URLs.
+
+ */
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+#include "plugin_testing_common.h"
+#include <iostream>
+
+#include "../RemapPluginInfo.h"
+
+PluginDebugObject debugObject;
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+TSReturnCode
+handleInitRun(char *errbuf, int errbuf_size, int &counter)
+{
+  TSReturnCode result = TS_SUCCESS;
+
+  if (debugObject.fail) {
+    result = TS_ERROR;
+    snprintf(errbuf, errbuf_size, "%s", "Init failed");
+  }
+
+  counter++;
+
+  return result;
+}
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+  TSReturnCode result = handleInitRun(errbuf, errbuf_size, debugObject.initCalled);
+  return result;
+}
+
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_size)
+{
+  TSReturnCode result = handleInitRun(errbuf, errbuf_size, debugObject.initInstanceCalled);
+
+  if (TS_SUCCESS == result) {
+    *ih = debugObject.input_ih;
+  }
+
+  debugObject.argc = argc;
+  debugObject.argv = argv;
+
+  return result;
+}
+
+void
+TSRemapDone(void)
+{
+  debugObject.doneCalled++;
+}
+
+void
+TSRemapDeleteInstance(void *ih)
+{
+  debugObject.deleteInstanceCalled++;
+  debugObject.ih = ih;
+}
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+  debugObject.doRemapCalled++;
+  return TSREMAP_NO_REMAP;
+}
+
+void
+TSRemapOSResponse(void *ih, TSHttpTxn rh, int os_response_type)
+{
+}
+
+void
+TSRemapConfigReload(void)
+{
+  debugObject.reloadConfigCalled++;
+}
+
+/* The folowing functions are meant for unit testing */
+int
+pluginDsoVersionTest()
+{
+#ifdef PLUGINDSOVER
+  return PLUGINDSOVER;
+#else
+  return -1;
+#endif
+}
+
+void *
+getPluginDebugObjectTest()
+{
+  return (void *)&debugObject;
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/plugin_testing_common.cc b/proxy/http/remap/unit-tests/plugin_testing_common.cc
new file mode 100644
index 0000000..d5d08d3
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_testing_common.cc
@@ -0,0 +1,39 @@
+/** @file
+
+  A test plugin common testing functionality
+
+  @section license License
+
+  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.
+
+  @section details Details
+
+  Implements code necessary for Reverse Proxy which mostly consists of
+  general purpose hostname substitution in URLs.
+
+ */
+
+#include "plugin_testing_common.h"
+
+void
+PrintToStdErr(const char *fmt, ...)
+{
+  va_list args;
+  va_start(args, fmt);
+  vfprintf(stderr, fmt, args);
+  va_end(args);
+}
diff --git a/proxy/http/remap/unit-tests/plugin_testing_common.h b/proxy/http/remap/unit-tests/plugin_testing_common.h
new file mode 100644
index 0000000..12346ea
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_testing_common.h
@@ -0,0 +1,95 @@
+/** @file
+
+  A test plugin header for testing Plugin's Dynamic Shared Objects (DSO)
+
+  @section license License
+
+  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.
+
+  @section details Details
+
+  Implements code necessary for Reverse Proxy which mostly consists of
+  general purpose hostname substitution in URLs.
+
+ */
+
+#pragma once
+
+#include <map>
+#include <string>
+#include <iostream>
+
+#include <stdio.h>
+#include <stdarg.h>
+
+#include "../PluginFactory.h"
+
+extern thread_local PluginThreadContext *pluginThreadContext;
+
+class PluginDebugObject
+{
+public:
+  PluginDebugObject() { clear(); }
+
+  void
+  clear()
+  {
+    contextInit          = nullptr;
+    contextInitInstance  = nullptr;
+    doRemapCalled        = 0;
+    initCalled           = 0;
+    doneCalled           = 0;
+    initInstanceCalled   = 0;
+    deleteInstanceCalled = 0;
+    reloadConfigCalled   = 0;
+    ih                   = nullptr;
+    argc                 = 0;
+    argv                 = nullptr;
+  }
+
+  /* Input fields used to set the test behavior of the plugin call-backs */
+  bool fail = false; /* tell the plugin call-back to fail for testing purposuses */
+  void *input_ih;    /* the value to be returned by the plugin instance init function */
+
+  /* Output fields showing what happend during the test */
+  const PluginThreadContext *contextInit         = nullptr; /* plugin initialization context */
+  const PluginThreadContext *contextInitInstance = nullptr; /* plugin instance initialization context */
+  int doRemapCalled                              = 0;       /* mark if remap was called */
+  int initCalled                                 = 0;       /* mark if plugin init was called */
+  int doneCalled                                 = 0;       /* mark if done was called */
+  int initInstanceCalled                         = 0;       /* mark if instance init was called */
+  int deleteInstanceCalled                       = 0;       /* mark if delete instance was called */
+  int reloadConfigCalled                         = 0;       /* mark if reload config was called */
+  void *ih                                       = nullptr; /* instance handler */
+  int argc                                       = 0;       /* number of plugin instance parameters received by the plugin */
+  char **argv                                    = nullptr; /* plugin instance parameters received by the plugin */
+};
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+typedef void *GetPluginDebugObjectFunction(void);
+GetPluginDebugObjectFunction getPluginDebugObjectTest;
+
+#define Debug(category, fmt, ...) PrintToStdErr("(%s) %s:%d:%s() " fmt "\n", category, __FILE__, __LINE__, __func__, ##__VA_ARGS__)
+#define Error(fmt, ...) PrintToStdErr("%s:%d:%s() " fmt "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__)
+void PrintToStdErr(const char *fmt, ...);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/test_PluginDso.cc b/proxy/http/remap/unit-tests/test_PluginDso.cc
new file mode 100644
index 0000000..c31e1d6
--- /dev/null
+++ b/proxy/http/remap/unit-tests/test_PluginDso.cc
@@ -0,0 +1,395 @@
+/** @file
+
+  Unit tests for a class that deals with plugin Dynamic Shared Objects (DSO)
+
+  @section license License
+
+  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.
+
+  @section details Details
+
+  Implements code necessary for Reverse Proxy which mostly consists of
+  general purpose hostname substitution in URLs.
+
+ */
+
+#define CATCH_CONFIG_MAIN /* include main function */
+#include <catch.hpp>      /* catch unit-test framework */
+#include <fstream>        /* ofstream */
+
+#include "plugin_testing_common.h"
+#include "../PluginDso.h"
+
+class PluginContext;
+thread_local PluginThreadContext *pluginThreadContext;
+
+std::error_code ec;
+
+/* A temp sandbox to play with our toys used for all fun with this test-bench */
+static fs::path tmpDir = fs::canonical(fs::temp_directory_path(), ec);
+
+/* The following are dirs that are used commonly in the unit-tests */
+static fs::path sandboxDir     = tmpDir / fs::path("sandbox");
+static fs::path runtimeDir     = sandboxDir / fs::path("runtime");
+static fs::path searchDir      = sandboxDir / fs::path("search");
+static fs::path pluginBuildDir = fs::current_path() / fs::path("unit-tests/.libs");
+
+/* The following are paths used in all scenarios in the unit tests */
+static fs::path configPath      = fs::path("plugin_v1.so");
+static fs::path pluginBuildPath = pluginBuildDir / configPath;
+static fs::path effectivePath   = searchDir / configPath;
+static fs::path runtimePath     = runtimeDir / configPath;
+
+void
+clean()
+{
+  fs::remove(sandboxDir, ec);
+}
+
+/* Mock used only to make PluginDso concrete enough to be tested */
+class PluginDsoUnitTest : public PluginDso
+{
+public:
+  PluginDsoUnitTest(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath)
+    : PluginDso(configPath, effectivePath, runtimePath)
+  {
+    /* don't remove runtime DSO copy preventively so we can check if it was created properly */
+    _preventiveCleaning = false;
+  }
+
+  virtual void
+  indicateReload()
+  {
+  }
+  virtual bool
+  init(std::string &error)
+  {
+    return true;
+  }
+  virtual void
+  done()
+  {
+  }
+};
+
+/*
+ * The following scenario tests loading and unloading of plugins
+ */
+SCENARIO("loading plugins", "[plugin][core]")
+{
+  clean();
+  std::string error;
+
+  GIVEN("a valid plugin")
+  {
+    /* Setup the test fixture - search, runtime dirs and install a plugin with some defined callback functions */
+    CHECK(fs::create_directories(searchDir, ec));
+    CHECK(fs::create_directories(runtimeDir, ec));
+    fs::copy(pluginBuildPath, searchDir, ec);
+
+    /* Instantiate and initialize a plugin DSO instance. Make sure effective path exists, used to load */
+    CHECK(fs::exists(effectivePath));
+    PluginDsoUnitTest plugin(configPath, effectivePath, runtimePath);
+
+    WHEN("loading a valid plugin")
+    {
+      bool result = plugin.load(error);
+
+      THEN("expect it to successfully load")
+      {
+        CHECK(true == result);
+        CHECK(error.empty());
+        CHECK(effectivePath == plugin.effectivePath());
+        CHECK(runtimePath == plugin.runtimePath());
+        CHECK(fs::exists(runtimePath));
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+
+    WHEN("loading a valid plugin")
+    {
+      bool result = plugin.load(error);
+
+      THEN("expect saving the right DSO file modification time")
+      {
+        CHECK(true == result);
+        CHECK(error.empty());
+        std::error_code ec;
+        fs::file_status fs = fs::status(effectivePath, ec);
+        CHECK(plugin.modTime() == fs::modification_time(fs));
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+
+    WHEN("loading a valid plugin but missing runtime dir")
+    {
+      CHECK(fs::remove(runtimeDir, ec));
+      CHECK_FALSE(fs::exists(runtimePath));
+      bool result = plugin.load(error);
+
+      THEN("expect it to fail")
+      {
+        CHECK_FALSE(true == result);
+        CHECK("failed to create a copy: No such file or directory" == error);
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+
+    WHEN("loading a valid plugin twice in a row")
+    {
+      /* First attempt OK */
+      bool result = plugin.load(error);
+      CHECK(true == result);
+      CHECK(error.empty());
+
+      /* Second attempt */
+      result = plugin.load(error);
+
+      THEN("expect it to fail the second attempt")
+      {
+        CHECK_FALSE(true == result);
+        CHECK("plugin already loaded" == error);
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+
+    WHEN("explicitly unloading a valid but not loaded plugin")
+    {
+      /* Make sure it is not loaded, runtime DSO not present */
+      CHECK_FALSE(fs::exists(runtimePath));
+
+      /* Unload w/o loading beforehand */
+      bool result = plugin.unload(error);
+
+      THEN("expect the unload to fail")
+      {
+        CHECK(false == result);
+        CHECK_FALSE(error.empty());
+        CHECK_FALSE(fs::exists(runtimePath));
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+
+    WHEN("unloading a valid plugin twice in a row")
+    {
+      /* First attempt OK */
+      bool result = plugin.load(error);
+      CHECK(true == result);
+      CHECK(error.empty());
+      result = plugin.unload(error);
+      CHECK(true == result);
+      CHECK("" == error);
+
+      /* Second attempt */
+      result = plugin.unload(error);
+
+      THEN("expect it to fail the second attempt")
+      {
+        CHECK_FALSE(true == result);
+        CHECK("no plugin loaded" == error);
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+
+    WHEN("explicitly unloading a valid and loaded plugin")
+    {
+      /* Make sure it is not loaded, runtime DSO not present */
+      CHECK_FALSE(fs::exists(runtimePath));
+
+      /* Load and make sure it is loaded */
+      CHECK(plugin.load(error));
+      /* Effective and runtime path set */
+      CHECK(effectivePath == plugin.effectivePath());
+      CHECK(runtimePath == plugin.runtimePath());
+      /* Runtime DSO should be present */
+      CHECK(fs::exists(runtimePath));
+
+      /* Unload */
+      bool result = plugin.unload(error);
+
+      THEN("expect it to successfully unload")
+      {
+        CHECK(true == result);
+        CHECK(error.empty());
+        /* Effective and runtime path still set */
+        CHECK(effectivePath == plugin.effectivePath());
+        CHECK(runtimePath == plugin.runtimePath());
+        /* Runtime DSO should not be found anymore */
+        CHECK_FALSE(fs::exists(runtimePath));
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+
+    WHEN("implicitly unloading a valid and loaded plugin")
+    {
+      {
+        PluginDsoUnitTest localPlugin(configPath, effectivePath, runtimePath);
+
+        /* Load and make sure it is loaded */
+        CHECK(localPlugin.load(error));
+        /* Effective and runtime path set */
+        CHECK(effectivePath == localPlugin.effectivePath());
+        CHECK(runtimePath == localPlugin.runtimePath());
+        /* Runtime DSO should be present */
+        CHECK(fs::exists(runtimePath));
+
+        /* Unload by going out of scope */
+      }
+
+      THEN("expect it to successfully unload and clean after itself")
+      {
+        /* Runtime path should be removed after unloading */
+        CHECK_FALSE(fs::exists(runtimePath));
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+  }
+
+  GIVEN("a plugin instance initialized with an empty effective path")
+  {
+    std::string error;
+    PluginDsoUnitTest plugin(configPath, /* effectivePath */ fs::path(), runtimePath);
+
+    WHEN("loading the plugin")
+    {
+      bool result = plugin.load(error);
+
+      THEN("expect the load to fail")
+      {
+        CHECK_FALSE(true == result);
+        CHECK("empty effective path" == error);
+        CHECK(plugin.effectivePath().empty());
+        CHECK(0 == plugin.modTime());
+        CHECK(runtimePath == plugin.runtimePath());
+        CHECK_FALSE(fs::exists(runtimePath));
+      }
+    }
+  }
+
+  GIVEN("an invalid plugin")
+  {
+    /* Create the directory structure and install plugins */
+    CHECK(fs::create_directories(searchDir, ec));
+    CHECK(fs::create_directories(runtimeDir, ec));
+    /* Create an invalid plugin and make sure the effective path to it exists */
+    std::ofstream file(effectivePath.string());
+    file << "Invalid plugin DSO content";
+    file.close();
+    CHECK(fs::exists(effectivePath));
+
+    /* Instantiate and initialize a plugin DSO instance. */
+    std::string error;
+    PluginDsoUnitTest plugin(configPath, effectivePath, runtimePath);
+
+    WHEN("loading an invalid plugin")
+    {
+      bool result = plugin.load(error);
+
+      THEN("expect it to fail to load")
+      {
+        /* After calling load() the following should be set correctly */
+        CHECK(effectivePath == plugin.effectivePath());
+        CHECK(runtimePath == plugin.runtimePath());
+
+        /* But the load should fail and an error should be returned */
+        CHECK(false == result);
+        CHECK_FALSE(error.empty());
+
+        /* Runtime DSO should not exist since the load failed. */
+        CHECK_FALSE(fs::exists(runtimePath));
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+  }
+}
+
+/*
+ * The following scenario tests finding symbols inside the DSO.
+ */
+SCENARIO("looking for symbols inside a plugin DSO", "[plugin][core]")
+{
+  clean();
+  std::string error;
+
+  /* Setup the test fixture - search, runtime dirs and install a plugin with some defined callback functions */
+  CHECK(fs::create_directories(searchDir, ec));
+  CHECK(fs::create_directories(runtimeDir, ec));
+  fs::copy(pluginBuildDir / configPath, searchDir, ec);
+
+  /* Initialize a plugin DSO instance */
+  PluginDsoUnitTest plugin(configPath, effectivePath, runtimePath);
+
+  /* Now test away. */
+  GIVEN("plugin loaded successfully")
+  {
+    CHECK(plugin.load(error));
+
+    WHEN("looking for an existing symbol")
+    {
+      THEN("expect to find it")
+      {
+        void *s = nullptr;
+        CHECK(plugin.getSymbol("TSRemapInit", s, error));
+        CHECK(nullptr != s);
+        CHECK(error.empty());
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+
+    WHEN("looking for non-existing symbol")
+    {
+      THEN("expect not to find it and get an error")
+      {
+        void *s = nullptr;
+        CHECK_FALSE(plugin.getSymbol("NONEXISTING_SYMBOL", s, error));
+        CHECK(nullptr == s);
+        CHECK_FALSE(error.empty());
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+
+    WHEN("looking for multiple existing symbols")
+    {
+      THEN("expect to find them all")
+      {
+        std::vector<const char *> list{"TSRemapInit",           "TSRemapDone",       "TSRemapDoRemap", "TSRemapNewInstance",
+                                       "TSRemapDeleteInstance", "TSRemapOSResponse", "TSPluginInit",   "pluginDsoVersionTest"};
+        for (auto symbol : list) {
+          void *s = nullptr;
+          CHECK(plugin.getSymbol(symbol, s, error));
+          CHECK(nullptr != s);
+          CHECK(error.empty());
+        }
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+
+    /* The following version function is used only for unit-testing of the plugin factory functionality */
+    WHEN("using a symbol to call the corresponding version function")
+    {
+      THEN("expect to return the version number")
+      {
+        void *s = nullptr;
+        CHECK(plugin.getSymbol("pluginDsoVersionTest", s, error));
+        int (*version)() = reinterpret_cast<int (*)()>(s);
+        int ver          = version ? version() : -1;
+        CHECK(1 == ver);
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+  }
+}
diff --git a/proxy/http/remap/unit-tests/test_PluginFactory.cc b/proxy/http/remap/unit-tests/test_PluginFactory.cc
new file mode 100644
index 0000000..c75040e
--- /dev/null
+++ b/proxy/http/remap/unit-tests/test_PluginFactory.cc
@@ -0,0 +1,657 @@
+/** @file
+
+  Unit tests for a class that deals with plugin Dynamic Shared Objects (DSO)
+
+  @section license License
+
+  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.
+
+  @section details Details
+
+  Implements code necessary for Reverse Proxy which mostly consists of
+  general purpose hostname substitution in URLs.
+
+ */
+
+#define CATCH_CONFIG_MAIN /* include main function */
+#include <catch.hpp>      /* catch unit-test framework */
+#include <fstream>        /* ofstream */
+#include <utime.h>
+
+#include "plugin_testing_common.h"
+#include "../PluginFactory.h"
+#include "../PluginDso.h"
+
+thread_local PluginThreadContext *pluginThreadContext;
+
+std::error_code ec;
+static void *INSTANCE_HANDLER = (void *)789;
+
+/* Mock of PluginFactory just to get consisten UUID to be able to test consistently */
+static fs::path tempComponent = fs::path("c71e2bab-90dc-4770-9535-c9304c3de38e");
+class PluginFactoryUnitTest : public PluginFactory
+{
+public:
+  PluginFactoryUnitTest(const fs::path &tempComponent)
+  {
+    _tempComponent      = tempComponent;
+    _preventiveCleaning = false;
+  }
+
+protected:
+  const char *
+  getUuid()
+  {
+    return _tempComponent.c_str();
+  }
+
+  fs::path _tempComponent;
+};
+
+PluginDebugObject *
+getDebugObject(const PluginDso &plugin)
+{
+  std::string error; /* ignore the error, return nullptr if symbol not defined */
+  void *address = nullptr;
+  plugin.getSymbol("getPluginDebugObjectTest", address, error);
+  GetPluginDebugObjectFunction *getObject = reinterpret_cast<GetPluginDebugObjectFunction *>(address);
+  if (getObject) {
+    PluginDebugObject *object = reinterpret_cast<PluginDebugObject *>(getObject());
+    return object;
+  } else {
+    return nullptr;
+  }
+}
+
+/* A temp sandbox to play with our toys used for all fun with this test-bench */
+static fs::path tmpDir = fs::canonical(fs::temp_directory_path(), ec);
+
+/* The following are paths that are used commonly in the unit-tests */
+static fs::path sandboxDir     = tmpDir / "sandbox";
+static fs::path runtimeRootDir = sandboxDir / "runtime";
+static fs::path runtimeDir     = runtimeRootDir / tempComponent;
+static fs::path searchDir      = sandboxDir / "search";
+static fs::path pluginBuildDir = fs::current_path() / "unit-tests/.libs";
+
+void
+clean()
+{
+  fs::remove(sandboxDir, ec);
+}
+
+static void
+setupConfigPathTest(const fs::path &configPath, const fs::path &pluginBuildPath, const fs::path &uuid, fs::path &effectivePath,
+                    fs::path &runtimePath, time_t mtime = 0, bool append = false)
+{
+  std::string error;
+  if (!append) {
+    clean();
+  }
+
+  effectivePath = configPath.is_absolute() ? configPath : searchDir / configPath;
+  runtimePath   = runtimeRootDir / uuid / effectivePath.relative_path();
+
+  /* Create the directory structure and install plugins */
+  fs::create_directories(effectivePath.parent_path(), ec);
+  fs::copy(pluginBuildPath, effectivePath, ec);
+  if (0 != mtime) {
+    struct stat sb;
+    struct utimbuf new_times;
+    stat(effectivePath.c_str(), &sb);
+    new_times.actime  = sb.st_atime; /* keep atime unchanged */
+    new_times.modtime = mtime;       /* set mtime to current time */
+    utime(effectivePath.c_str(), &new_times);
+  }
+
+  CHECK(fs::exists(effectivePath));
+}
+
+static PluginFactoryUnitTest *
+getFactory(const fs::path &uuid)
+{
+  /* Instantiate and initialize a plugin factory. */
+  PluginFactoryUnitTest *factory = new PluginFactoryUnitTest(uuid);
+  factory->setRuntimeDir(runtimeRootDir);
+  factory->addSearchDir(searchDir);
+  return factory;
+}
+
+static void
+teardownConfigPathTest(PluginFactoryUnitTest *factory)
+{
+  delete factory;
+  clean();
+}
+
+static void
+validateSuccessfulConfigPathTest(const RemapPluginInst *pluginInst, const std::string &error, const fs::path &effectivePath,
+                                 const fs::path &runtimePath)
+{
+  CHECK(nullptr != pluginInst);
+  CHECK("" == error);
+  CHECK(effectivePath == pluginInst->_plugin.effectivePath());
+  CHECK(runtimePath == pluginInst->_plugin.runtimePath());
+}
+
+SCENARIO("loading plugins", "[plugin][core]")
+{
+  fs::path effectivePath;
+  fs::path runtimePath;
+  std::string error;
+
+  GIVEN("an existing plugin")
+  {
+    fs::path pluginName = fs::path("plugin_v1.so");
+    fs::path buildPath  = pluginBuildDir / pluginName;
+
+    WHEN("config using plugin file name only")
+    {
+      fs::path configPath = pluginName;
+      CHECK(configPath.is_relative()); /* make sure this is relative path - this is what we are testing */
+
+      setupConfigPathTest(configPath, buildPath, tempComponent, effectivePath, runtimePath);
+      PluginFactoryUnitTest *factory = getFactory(tempComponent);
+      RemapPluginInst *plugin        = factory->getRemapPlugin(configPath, 0, nullptr, error);
+
+      THEN("expect it to successfully load") { validateSuccessfulConfigPathTest(plugin, error, effectivePath, runtimePath); }
+
+      teardownConfigPathTest(factory);
+    }
+
+    WHEN("config is using plugin relative filename")
+    {
+      fs::path configPath = fs::path("subdir") / pluginName;
+      CHECK(configPath.is_relative()); /* make sure this is relative path - this is what we are testing */
+
+      setupConfigPathTest(configPath, buildPath, tempComponent, effectivePath, runtimePath);
+      PluginFactoryUnitTest *factory = getFactory(tempComponent);
+      RemapPluginInst *plugin        = factory->getRemapPlugin(configPath, 0, nullptr, error);
+
+      THEN("expect it to successfully load") { validateSuccessfulConfigPathTest(plugin, error, effectivePath, runtimePath); }
+
+      teardownConfigPathTest(factory);
+    }
+
+    WHEN("config is using plugin absolute path")
+    {
+      fs::path configPath = searchDir / "subdir" / pluginName;
+      CHECK(configPath.is_absolute()); /* make sure this is absolute path - this is what we are testing */
+
+      setupConfigPathTest(configPath, buildPath, tempComponent, effectivePath, runtimePath);
+      PluginFactoryUnitTest *factory = getFactory(tempComponent);
+      RemapPluginInst *plugin        = factory->getRemapPlugin(configPath, 0, nullptr, error);
+
+      THEN("expect it to successfully load") { validateSuccessfulConfigPathTest(plugin, error, effectivePath, runtimePath); }
+
+      teardownConfigPathTest(factory);
+    }
+
+    WHEN("config using nonexisting relative plugin file name")
+    {
+      fs::path relativeExistingPath = pluginName;
+      CHECK(relativeExistingPath.is_relative());
+      fs::path relativeNonexistingPath("subdir");
+      relativeNonexistingPath /= fs::path("nonexisting_plugin.so");
+      CHECK(relativeNonexistingPath.is_relative());
+
+      setupConfigPathTest(relativeExistingPath, buildPath, tempComponent, effectivePath, runtimePath);
+      PluginFactoryUnitTest *factory = getFactory(tempComponent);
+      RemapPluginInst *plugin        = factory->getRemapPlugin(relativeNonexistingPath, 0, nullptr, error);
+
+      THEN("expect it to fail with appropriate error message")
+      {
+        std::string expectedError;
+        expectedError.append("failed to find plugin '").append(relativeNonexistingPath.string()).append("'");
+        CHECK(nullptr == plugin);
+        CHECK(expectedError == error);
+      }
+
+      teardownConfigPathTest(factory);
+    }
+
+    WHEN("config using nonexisting absolute plugin file name")
+    {
+      fs::path relativeExistingPath = pluginName;
+      CHECK(relativeExistingPath.is_relative());
+      fs::path absoluteNonexistingPath = searchDir / "subdir" / "nonexisting_plugin.so";
+      CHECK(absoluteNonexistingPath.is_absolute());
+
+      setupConfigPathTest(relativeExistingPath, buildPath, tempComponent, effectivePath, runtimePath);
+      PluginFactoryUnitTest *factory = getFactory(tempComponent);
+      RemapPluginInst *plugin        = factory->getRemapPlugin(absoluteNonexistingPath, 0, nullptr, error);
+
+      THEN("expect it to fail with appropriate error message")
+      {
+        std::string expectedError;
+        expectedError.append("failed to find plugin '").append(absoluteNonexistingPath.string()).append("'");
+        CHECK(nullptr == plugin);
+        CHECK(expectedError == error);
+      }
+
+      teardownConfigPathTest(factory);
+    }
+  }
+}
+
+SCENARIO("multiple search dirs + multiple or no plugins installed", "[plugin][core]")
+{
+  GIVEN("multiple search dirs specified for the plugin search")
+  {
+    /* Create the directory structure and install plugins */
+    fs::path configPath              = fs::path("plugin_v1.so");
+    fs::path pluginName              = fs::path("plugin_v1.so");
+    fs::path searchDir1              = sandboxDir / "search1";
+    fs::path searchDir2              = sandboxDir / "search2";
+    fs::path searchDir3              = sandboxDir / "search3";
+    std::vector<fs::path> searchDirs = {searchDir1, searchDir2, searchDir3};
+    fs::path effectivePath1          = searchDir1 / configPath;
+    fs::path effectivePath2          = searchDir2 / configPath;
+    fs::path effectivePath3          = searchDir3 / configPath;
+    fs::path runtimePath1            = runtimeDir / effectivePath1.relative_path();
+    fs::path runtimePath2            = runtimeDir / effectivePath2.relative_path();
+    fs::path runtimePath3            = runtimeDir / effectivePath3.relative_path();
+    fs::path pluginBuildPath         = fs::current_path() / fs::path("unit-tests/.libs") / pluginName;
+
+    std::string error;
+
+    for (auto searchDir : searchDirs) {
+      CHECK(fs::create_directories(searchDir, ec));
+      fs::copy(pluginBuildPath, searchDir, ec);
+    }
+    CHECK(fs::create_directories(runtimeDir, ec));
+
+    /* Instantiate and initialize a plugin DSO instance. */
+    PluginFactoryUnitTest factory(tempComponent);
+    factory.setRuntimeDir(runtimeRootDir);
+    for (auto searchDir : searchDirs) {
+      factory.addSearchDir(searchDir);
+    }
+
+    CHECK(fs::exists(effectivePath1));
+    CHECK(fs::exists(effectivePath2));
+    CHECK(fs::exists(effectivePath3));
+
+    WHEN("loading an existing plugin using its absolute path but the plugin is not located in any of the search dirs")
+    {
+      /* Prepare "unregistered" directory containing a valid plugin but not registered with the factory as a search directory */
+      fs::path unregisteredDir = sandboxDir / searchDir / "unregistered";
+      CHECK(fs::create_directories(unregisteredDir, ec));
+      fs::copy(pluginBuildPath, unregisteredDir, ec);
+      fs::path abEffectivePath = unregisteredDir / pluginName;
+      fs::path absRuntimePath  = runtimeDir / abEffectivePath.relative_path();
+      CHECK(abEffectivePath.is_absolute());
+      CHECK(fs::exists(abEffectivePath));
+
+      /* Now use an absolute path containing the unregistered search directory */
+      RemapPluginInst *pluginInst = factory.getRemapPlugin(abEffectivePath, 0, nullptr, error);
+
+      THEN("Expect it to successfully load")
+      {
+        CHECK(nullptr != pluginInst);
+        CHECK(error.empty());
+        CHECK(abEffectivePath == pluginInst->_plugin.effectivePath());
+        CHECK(absRuntimePath == pluginInst->_plugin.runtimePath());
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+
+    WHEN("a valid plugin is found in the first search path")
+    {
+      RemapPluginInst *pluginInst = factory.getRemapPlugin(configPath, 0, nullptr, error);
+
+      THEN("Expect it to successfully load the one found in the first search dir and copy it in the runtime dir")
+      {
+        CHECK(nullptr != pluginInst);
+        CHECK(error.empty());
+        CHECK(effectivePath1 == pluginInst->_plugin.effectivePath());
+        CHECK(runtimePath1 == pluginInst->_plugin.runtimePath());
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+
+    WHEN("the first search dir is missing the plugin but the second search has it")
+    {
+      CHECK(fs::remove(effectivePath1, ec));
+      RemapPluginInst *pluginInst = factory.getRemapPlugin(configPath, 0, nullptr, error);
+
+      THEN("Expect it to successfully load the one found in the second search dir")
+      {
+        CHECK(nullptr != pluginInst);
+        CHECK(error.empty());
+        CHECK(effectivePath2 == pluginInst->_plugin.effectivePath());
+        CHECK(runtimePath2 == pluginInst->_plugin.runtimePath());
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+
+    WHEN("the first and second search dir are missing the plugin but the third search has it")
+    {
+      CHECK(fs::remove(effectivePath1, ec));
+      CHECK(fs::remove(effectivePath2, ec));
+      RemapPluginInst *pluginInst = factory.getRemapPlugin(configPath, 0, nullptr, error);
+
+      THEN("Expect it to successfully load the one found in the third search dir")
+      {
+        CHECK(nullptr != pluginInst);
+        CHECK(error.empty());
+        CHECK(effectivePath3 == pluginInst->_plugin.effectivePath());
+        CHECK(runtimePath3 == pluginInst->_plugin.runtimePath());
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+
+    WHEN("none of the search dirs contains a valid plugin")
+    {
+      CHECK(fs::remove(effectivePath1, ec));
+      CHECK(fs::remove(effectivePath2, ec));
+      CHECK(fs::remove(effectivePath3, ec));
+
+      THEN("expect the plugin load to fail.")
+      {
+        RemapPluginInst *pluginInst = factory.getRemapPlugin(configPath, 0, nullptr, error);
+        CHECK(nullptr == pluginInst);
+        CHECK(std::string("failed to find plugin '").append(configPath.string()).append("'") == error);
+        CHECK_FALSE(fs::exists(runtimePath1));
+        CHECK_FALSE(fs::exists(runtimePath2));
+        CHECK_FALSE(fs::exists(runtimePath3));
+      }
+      CHECK(fs::remove(sandboxDir, ec));
+    }
+  }
+}
+
+static int
+getPluginVersion(const PluginDso &plugin)
+{
+  std::string error;
+  void *s = nullptr;
+  CHECK(plugin.getSymbol("pluginDsoVersionTest", s, error));
+  int (*version)() = reinterpret_cast<int (*)()>(s);
+  return version ? version() : -1;
+}
+
+SCENARIO("loading multiple version of the same plugin at the same time", "[plugin][core]")
+{
+  static fs::path uuid_t1 = fs::path("c71e2bab-90dc-4770-9535-c9304c3de381"); /* UUID at moment t1 */
+  static fs::path uuid_t2 = fs::path("c71e2bab-90dc-4770-9535-e7304c3ee732"); /* UUID at moment t2 */
+
+  fs::path effectivePath_v1;            /* expected effective path for DSO v1 */
+  fs::path effectivePath_v2;            /* expected effective path for DSO v2 */
+  fs::path runtimePath_v1;              /* expected runtime path for DSO v1 */
+  fs::path runtimePath_v2;              /* expected runtime path for DSO v2 */
+  void *tsRemapInitSym_v1_t1 = nullptr; /* callback address from DSO v1 at moment t1 */
+  void *tsRemapInitSym_v1_t2 = nullptr; /* callback address from DSO v1 at moment t2 */
+  void *tsRemapInitSym_v2_t2 = nullptr; /* callback address from DSO v2 at moment t2 */
+
+  std::string error;
+  std::string error1;
+  std::string error2;
+
+  fs::path configName   = fs::path("plugin.so");                     /* use same config name for all following tests */
+  fs::path buildPath_v1 = pluginBuildDir / fs::path("plugin_v1.so"); /* DSO v1 */
+  fs::path buildPath_v2 = pluginBuildDir / fs::path("plugin_v2.so"); /* DSO v1 */
+
+  GIVEN("two different versions v1 and v2 of same plugin")
+  {
+    WHEN("(1) loading v1, (2) overwriting with v2 and then (3) reloading by using the same plugin name, "
+         "(*) v1 and v2 DSOs modification time are different (changed)")
+    {
+      /* Simulate installing plugin plugin_v1.so (ver 1) as plugin.so and loading it at some point of time t1 */
+      setupConfigPathTest(configName, buildPath_v1, uuid_t1, effectivePath_v1, runtimePath_v1, 1556825556);
+      PluginFactoryUnitTest *factory1 = getFactory(uuid_t1);
+      RemapPluginInst *plugin_v1      = factory1->getRemapPlugin(configName, 0, nullptr, error1);
+      plugin_v1->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v1_t1, error);
+
+      /* Simulate installing plugin plugin_v2.so (v1) as plugin.so and loading it at some point of time t2 */
+      /* Note that during the installation plugin_v2.so (v2) is "barberically" overriding the existing plugin.so which was v1 */
+      setupConfigPathTest(configName, buildPath_v2, uuid_t2, effectivePath_v2, runtimePath_v2, 1556825557);
+      PluginFactoryUnitTest *factory2 = getFactory(uuid_t2);
+      RemapPluginInst *plugin_v2      = factory2->getRemapPlugin(configName, 0, nullptr, error2);
+
+      /* Make sure plugin.so was overriden */
+      CHECK(effectivePath_v1 == effectivePath_v2);
+
+      /* Although effective path is the same runtime paths should be different */
+      CHECK(runtimePath_v1 != runtimePath_v2);
+
+      THEN("expect both to be successfully loaded and used simultaneously")
+      {
+        /* Both loadings should succeed */
+        validateSuccessfulConfigPathTest(plugin_v1, error1, effectivePath_v1, runtimePath_v1);
+        validateSuccessfulConfigPathTest(plugin_v2, error2, effectivePath_v2, runtimePath_v2);
+
+        /* Make sure what we installed and loaded first was v1 and after the plugin reload we run v2 */
+        CHECK(1 == getPluginVersion(plugin_v1->_plugin));
+        CHECK(2 == getPluginVersion(plugin_v2->_plugin));
+
+        /* Make sure the symbols we get from the 2 loaded plugins don't yield the same callback function pointer */
+        plugin_v1->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v1_t2, error);
+        plugin_v2->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v2_t2, error);
+        CHECK(nullptr != tsRemapInitSym_v1_t2);
+        CHECK(nullptr != tsRemapInitSym_v2_t2);
+        CHECK(tsRemapInitSym_v1_t2 != tsRemapInitSym_v2_t2);
+
+        /* Make sure v1 callback functions addresses did not change for v1 after v2 was loaded */
+        CHECK(tsRemapInitSym_v1_t1 == tsRemapInitSym_v1_t2);
+      }
+
+      teardownConfigPathTest(factory1);
+      teardownConfigPathTest(factory2);
+    }
+  }
+
+  GIVEN("two different versions v1 and v2 of same plugin")
+  {
+    WHEN("(1) loading v1, (2) overwriting with v2 and then (3) reloading by using the same plugin name, "
+         "(*) v1 and v2 DSOs modification time are same (did NOT change)")
+    {
+      /* Simulate installing plugin plugin_v1.so (ver 1) as plugin.so and loading it at some point of time t1 */
+      setupConfigPathTest(configName, buildPath_v1, uuid_t1, effectivePath_v1, runtimePath_v1, 1556825556);
+      PluginFactoryUnitTest *factory1 = getFactory(uuid_t1);
+      RemapPluginInst *plugin_v1      = factory1->getRemapPlugin(configName, 0, nullptr, error1);
+
+      /* Simulate installing plugin plugin_v2.so (v1) as plugin.so and loading it at some point of time t2 */
+      /* Note that during the installation plugin_v2.so (v2) is "barberically" overriding the existing plugin.so
+         which was v1, since the modification time is exactly the same the new v2 plugin would not be loaded and
+         we should get the same PluginDso address and same effective and runtime paths */
+      setupConfigPathTest(configName, buildPath_v2, uuid_t2, effectivePath_v2, runtimePath_v2, 1556825556);
+      PluginFactoryUnitTest *factory2 = getFactory(uuid_t2);
+      RemapPluginInst *plugin_v2      = factory2->getRemapPlugin(configName, 0, nullptr, error2);
+
+      /* Make sure plugin.so was overriden */
+      CHECK(effectivePath_v1 == effectivePath_v2);
+
+      THEN("expect only v1 plugin to be loaded since the timestamp has not changed")
+      {
+        /* Both getRemapPlugin() calls should succeed but only v1 plugin DSO should be used */
+        validateSuccessfulConfigPathTest(plugin_v1, error1, effectivePath_v1, runtimePath_v1);
+        validateSuccessfulConfigPathTest(plugin_v2, error2, effectivePath_v2, runtimePath_v1);
+
+        /* Make sure we ended up with the same DSO object and runtime paths should be same - no new plugin was loaded */
+        CHECK(&(plugin_v1->_plugin) == &(plugin_v2->_plugin));
+        CHECK(plugin_v1->_plugin.runtimePath() == plugin_v2->_plugin.runtimePath());
+
+        /* Make sure v2 DSO was NOT loaded both instances should return same v1 version */
+        CHECK(1 == getPluginVersion(plugin_v1->_plugin));
+        CHECK(1 == getPluginVersion(plugin_v2->_plugin));
+
+        /* Make sure the symbols we get from the 2 loaded plugins yield the same callback function pointer */
+        plugin_v1->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v1_t2, error);
+        plugin_v2->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v2_t2, error);
+        CHECK(nullptr != tsRemapInitSym_v1_t2);
+        CHECK(nullptr != tsRemapInitSym_v2_t2);
+        CHECK(tsRemapInitSym_v1_t2 == tsRemapInitSym_v2_t2);
+      }
+
+      teardownConfigPathTest(factory1);
+      teardownConfigPathTest(factory2);
+    }
+  }
+
+  /* Since factories share the list of loaded plugins to avoid unnecessary loading of unchanged plugins
+   * lets check if destroying a factory impacts plugins loaded from another factory */
+  GIVEN("configurations with and without plugins")
+  {
+    WHEN("loading a configuration without plugins and then reloading configuration with a plugin")
+    {
+      /* Simulate configuration without plugins - an unused factory */
+      PluginFactoryUnitTest *factory1 = getFactory(uuid_t1);
+
+      /* Now provision and load a plugin using a second factory */
+      setupConfigPathTest(configName, buildPath_v2, uuid_t2, effectivePath_v2, runtimePath_v2, 1556825556);
+      PluginFactoryUnitTest *factory2 = getFactory(uuid_t2);
+      RemapPluginInst *plugin_v2      = factory2->getRemapPlugin(configName, 0, nullptr, error2);
+
+      THEN("the plugin from the second factory to work")
+      {
+        validateSuccessfulConfigPathTest(plugin_v2, error2, effectivePath_v2, runtimePath_v2);
+
+        /* Now delete the first factory and call a plugin from the second factory */
+        delete factory1;
+        CHECK(TSREMAP_NO_REMAP == plugin_v2->_plugin.doRemap(INSTANCE_HANDLER, nullptr, nullptr));
+      }
+
+      teardownConfigPathTest(factory2);
+    }
+  }
+}
+
+SCENARIO("notifying plugins of config reload", "[plugin][core]")
+{
+  /* use 2 copies of the same plugin to test */
+  fs::path configName1 = fs::path("plugin_testing_calls_1.so");
+  fs::path configName2 = fs::path("plugin_testing_calls_2.so");
+  fs::path buildPath   = pluginBuildDir / fs::path("plugin_testing_calls.so");
+
+  static fs::path uuid_t1 = fs::path("c71e2bab-90dc-4770-9535-c9304c3de381"); /* UUID at moment t1 */
+  static fs::path uuid_t2 = fs::path("c71e2bab-90dc-4770-9535-e7304c3ee732"); /* UUID at moment t2 */
+
+  fs::path effectivePath1;
+  fs::path effectivePath2;
+  fs::path runtimePath1;
+  fs::path runtimePath2;
+
+  std::string error;
+
+  GIVEN("simple configuration with 1 plugin and 1 factory")
+  {
+    WHEN("indicating config reload")
+    {
+      /* Simulate configuration without plugins - an unused factory */
+      setupConfigPathTest(configName1, buildPath, uuid_t1, effectivePath1, runtimePath1, 1556825556);
+      PluginFactoryUnitTest *factory1 = getFactory(uuid_t1);
+      RemapPluginInst *plugin1        = factory1->getRemapPlugin(configName1, 0, nullptr, error);
+
+      /* check if loaded successfully */
+      validateSuccessfulConfigPathTest(plugin1, error, effectivePath1, runtimePath1);
+
+      /* Prapare the debug object */
+      PluginDebugObject *debugObject = getDebugObject(plugin1->_plugin);
+      debugObject->clear();
+
+      THEN("expect 'done' methods to be called for plugin and the instance but not the 'reload config' methods")
+      {
+        /* Simulate reloading the config */
+        factory1->indicateReload();
+
+        /* was "done" method called? */
+        CHECK(1 == debugObject->doneCalled);
+        CHECK(1 == debugObject->deleteInstanceCalled);
+        CHECK(0 == debugObject->reloadConfigCalled);
+      }
+
+      teardownConfigPathTest(factory1);
+    }
+  }
+
+  GIVEN("configuration with 2 plugins loaded by 1 factory")
+  {
+    WHEN("indicating config reload")
+    {
+      /* Simulate configuration without plugins - an unused factory */
+      setupConfigPathTest(configName1, buildPath, uuid_t1, effectivePath1, runtimePath1, 1556825556);
+      setupConfigPathTest(configName2, buildPath, uuid_t1, effectivePath2, runtimePath2, 1556825556, /* append */ true);
+      PluginFactoryUnitTest *factory1 = getFactory(uuid_t1);
+      RemapPluginInst *plugin1        = factory1->getRemapPlugin(configName1, 0, nullptr, error);
+      RemapPluginInst *plugin2        = factory1->getRemapPlugin(configName2, 0, nullptr, error);
+
+      /* check if loaded successfully */
+      validateSuccessfulConfigPathTest(plugin1, error, effectivePath1, runtimePath1);
+      validateSuccessfulConfigPathTest(plugin2, error, effectivePath2, runtimePath2);
+
+      /* Prapare the debug objects */
+      PluginDebugObject *debugObject1 = getDebugObject(plugin1->_plugin);
+      PluginDebugObject *debugObject2 = getDebugObject(plugin2->_plugin);
+      debugObject1->clear();
+      debugObject2->clear();
+
+      THEN("expect 'done' methods to be called but not the 'reload config' methods")
+      {
+        /* Simulate reloading the config */
+        factory1->indicateReload();
+
+        /* Was "done" method called? */
+        CHECK(1 == debugObject1->doneCalled);
+        CHECK(1 == debugObject1->deleteInstanceCalled);
+        CHECK(0 == debugObject1->reloadConfigCalled);
+        CHECK(1 == debugObject2->doneCalled);
+        CHECK(1 == debugObject2->deleteInstanceCalled);
+        CHECK(0 == debugObject2->reloadConfigCalled);
+      }
+
+      teardownConfigPathTest(factory1);
+    }
+  }
+
+  GIVEN("configuration with 1 plugin loaded by 2 separate factories")
+  {
+    WHEN("indicating config reload")
+    {
+      /* Simulate configuration without plugins - an unused factory */
+      setupConfigPathTest(configName1, buildPath, uuid_t1, effectivePath1, runtimePath1, 1556825556);
+      PluginFactoryUnitTest *factory1 = getFactory(uuid_t1);
+      PluginFactoryUnitTest *factory2 = getFactory(uuid_t2);
+      RemapPluginInst *plugin1        = factory1->getRemapPlugin(configName1, 0, nullptr, error);
+      RemapPluginInst *plugin2        = factory2->getRemapPlugin(configName1, 0, nullptr, error);
+
+      /* Prapare the debug objects */
+      PluginDebugObject *debugObject1 = getDebugObject(plugin1->_plugin);
+      PluginDebugObject *debugObject2 = getDebugObject(plugin2->_plugin);
+
+      THEN("expect instance 'done' to be always called, but plugin 'done' called only after destroying one factory")
+      {
+        debugObject2->clear();
+        factory2->indicateReload();
+        CHECK(0 == debugObject2->doneCalled);
+        CHECK(1 == debugObject2->deleteInstanceCalled);
+        CHECK(1 == debugObject2->reloadConfigCalled);
+
+        delete factory2;
+
+        debugObject1->clear();
+        factory1->indicateReload();
+        CHECK(1 == debugObject1->doneCalled);
+        CHECK(1 == debugObject1->deleteInstanceCalled);
+        CHECK(0 == debugObject1->reloadConfigCalled);
+
+        delete factory1;
+      }
+
+      clean();
+    }
+  }
+}
diff --git a/proxy/http/remap/unit-tests/test_RemapPlugin.cc b/proxy/http/remap/unit-tests/test_RemapPlugin.cc
new file mode 100644
index 0000000..0eedcaf
--- /dev/null
+++ b/proxy/http/remap/unit-tests/test_RemapPlugin.cc
@@ -0,0 +1,433 @@
+/** @file
+
+  Unit tests for a class that deals with remap plugins
+
+  @section license License
+
+  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.
+
+  @section details Details
+
+  Implements code necessary for Reverse Proxy which mostly consists of
+  general purpose hostname substitution in URLs.
+
+ */
+
+#define CATCH_CONFIG_MAIN /* include main function */
+#include <catch.hpp>      /* catch unit-test framework */
+#include <fstream>        /* ofstream */
+#include <string>
+
+#include "plugin_testing_common.h"
+#include "../RemapPluginInfo.h"
+
+thread_local PluginThreadContext *pluginThreadContext;
+
+static void *INSTANCE_HANDLER = (void *)789;
+std::error_code ec;
+
+/* Some plugin context pointers used for unit testing */
+// static const PluginThreadContext *PLUGIN_INIT_CONTEXT_CUR    = (PluginThreadContext *)1;
+// static const PluginThreadContext *PLUGIN_INIT_CONTEXT_NEW_V1 = (PluginThreadContext *)2;
+// static const PluginThreadContext *PLUGIN_INIT_CONTEXT_NEW_V2 = (PluginThreadContext *)3;
+
+/* A temp sandbox to play with our toys used for all fun with this test-bench */
+static fs::path tmpDir = fs::canonical(fs::temp_directory_path(), ec);
+
+/* The following are paths that are used commonly in the unit-tests */
+static fs::path sandboxDir     = tmpDir / "sandbox";
+static fs::path runtimeDir     = sandboxDir / "runtime";
+static fs::path searchDir      = sandboxDir / "search";
+static fs::path pluginBuildDir = fs::current_path() / "unit-tests/.libs";
+
+void
+clean()
+{
+  fs::remove(sandboxDir, ec);
+}
+
+/* Mock used only to make unit testing convenient to check if callbacks are really called and check errors */
+class RemapPluginUnitTest : public RemapPluginInfo
+{
+public:
+  RemapPluginUnitTest(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath)
+    : RemapPluginInfo(configPath, effectivePath, runtimePath)
+  {
+  }
+  std::string
+  getError(const char *required, const char *requiring = nullptr)
+  {
+    return missingRequiredSymbolError(_configPath.string(), required, requiring);
+  }
+
+  PluginDebugObject *
+  getDebugObject()
+  {
+    std::string error; /* ignore the error, return nullptr if symbol not defined */
+    void *address = nullptr;
+    getSymbol("getPluginDebugObjectTest", address, error);
+    GetPluginDebugObjectFunction *getObject = reinterpret_cast<GetPluginDebugObjectFunction *>(address);
+    if (getObject) {
+      PluginDebugObject *object = reinterpret_cast<PluginDebugObject *>(getObject());
+      return object;
+    } else {
+      return nullptr;
+    }
+  }
+};
+
+RemapPluginUnitTest *
+setupSandBox(const fs::path configPath)
+{
+  std::string error;
+  clean();
+
+  /* Create the directory structure and install plugins */
+  CHECK(fs::create_directories(searchDir, ec));
+  fs::copy(pluginBuildDir / configPath, searchDir, ec);
+  CHECK(fs::create_directories(runtimeDir, ec));
+
+  fs::path effectivePath   = searchDir / configPath;
+  fs::path runtimePath     = runtimeDir / configPath;
+  fs::path pluginBuildPath = pluginBuildDir / configPath;
+
+  /* Instantiate and initialize a plugin DSO instance. */
+  RemapPluginUnitTest *plugin = new RemapPluginUnitTest(configPath, effectivePath, runtimePath);
+
+  return plugin;
+}
+
+bool
+loadPlugin(RemapPluginUnitTest *plugin, std::string &error, PluginDebugObject *&debugObject)
+{
+  bool result = plugin->load(error);
+  debugObject = plugin->getDebugObject();
+  return result;
+}
+
+void
+cleanupSandBox(RemapPluginInfo *plugin)
+{
+  delete plugin;
+  clean();
+}
+
+SCENARIO("loading remap plugins", "[plugin][core]")
+{
+  std::string error;
+  PluginDebugObject *debugObject = nullptr;
+
+  GIVEN("a plugin which has only minimum required call back functions")
+  {
+    fs::path pluginConfigPath   = fs::path("plugin_required_cb.so");
+    RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+    WHEN("loading")
+    {
+      bool result = loadPlugin(plugin, error, debugObject);
+
+      THEN("expect it to successfully load")
+      {
+        CHECK(true == result);
+        CHECK(error.empty());
+      }
+      cleanupSandBox(plugin);
+    }
+  }
+
+  GIVEN("a plugin which is missing the plugin TSREMAP_FUNCNAME_INIT function")
+  {
+    fs::path pluginConfigPath   = fs::path("plugin_missing_init.so");
+    RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+    WHEN("loading")
+    {
+      bool result = loadPlugin(plugin, error, debugObject);
+
+      THEN("expect it to successfully load")
+      {
+        CHECK_FALSE(result);
+        CHECK(error == plugin->getError(TSREMAP_FUNCNAME_INIT));
+      }
+      cleanupSandBox(plugin);
+    }
+  }
+
+  GIVEN("a plugin which is missing the TSREMAP_FUNCNAME_DO_REMAP function")
+  {
+    fs::path pluginConfigPath   = fs::path("plugin_missing_doremap.so");
+    RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+    WHEN("loading")
+    {
+      bool result = loadPlugin(plugin, error, debugObject);
+
+      THEN("expect it to fail")
+      {
+        CHECK_FALSE(result);
+        CHECK(error == plugin->getError(TSREMAP_FUNCNAME_DO_REMAP));
+      }
+      cleanupSandBox(plugin);
+    }
+  }
+
+  GIVEN("a plugin which has TSREMAP_FUNCNAME_NEW_INSTANCE but is missing the TSREMAP_FUNCNAME_DELETE_INSTANCE function")
+  {
+    fs::path pluginConfigPath   = fs::path("plugin_missing_deleteinstance.so");
+    RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+    WHEN("loading")
+    {
+      bool result = loadPlugin(plugin, error, debugObject);
+
+      THEN("expect it to fail")
+      {
+        CHECK_FALSE(result);
+        CHECK(error == plugin->getError(TSREMAP_FUNCNAME_DELETE_INSTANCE, TSREMAP_FUNCNAME_NEW_INSTANCE));
+      }
+      cleanupSandBox(plugin);
+    }
+  }
+
+  GIVEN("a plugin which has TSREMAP_FUNCNAME_DELETE_INSTANCE but is missing the TSREMAP_FUNCNAME_NEW_INSTANCE function")
+  {
+    fs::path pluginConfigPath   = fs::path("plugin_missing_newinstance.so");
+    RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+    WHEN("loading")
+    {
+      bool result = loadPlugin(plugin, error, debugObject);
+
+      THEN("expect it to fail")
+      {
+        CHECK_FALSE(result);
+        CHECK(error == plugin->getError(TSREMAP_FUNCNAME_NEW_INSTANCE, TSREMAP_FUNCNAME_DELETE_INSTANCE));
+      }
+      cleanupSandBox(plugin);
+    }
+  }
+}
+
+void
+prepCallTest(bool toFail, PluginDebugObject *debugObject)
+{
+  debugObject->clear();
+  debugObject->fail = toFail; // Tell the mock init to succeed or succeed.
+}
+
+void
+checkCallTest(bool shouldHaveFailed, bool result, const std::string &error, std::string &expectedError, int &called)
+{
+  CHECK(1 == called); // Init was called.
+  if (shouldHaveFailed) {
+    CHECK(false == result);
+    CHECK(error == expectedError); // Appropriate error was returned.
+  } else {
+    CHECK(true == result); // Init succesfull - returned TS_SUCCESS.
+    CHECK(error.empty());  // No error was returned.
+  }
+}
+
+SCENARIO("invoking plugin init", "[plugin][core]")
+{
+  std::string error;
+  PluginDebugObject *debugObject = nullptr;
+
+  GIVEN("plugin init function")
+  {
+    fs::path pluginConfigPath   = fs::path("plugin_testing_calls.so");
+    RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+    bool result = loadPlugin(plugin, error, debugObject);
+    CHECK(true == result);
+
+    WHEN("init succeeds")
+    {
+      prepCallTest(/* toFail */ false, debugObject);
+
+      result = plugin->init(error);
+
+      THEN("expect init to be called, success code and no error to be returned")
+      {
+        std::string expectedError;
+
+        checkCallTest(/* shouldHaveFailed */ false, result, error, expectedError, debugObject->initCalled);
+      }
+      cleanupSandBox(plugin);
+    }
+
+    WHEN("init fails")
+    {
+      prepCallTest(/* toFail */ true, debugObject);
+
+      result = plugin->init(error);
+
+      THEN("expect init to be called, failure code and an error to be returned")
+      {
+        std::string expectedError;
+        expectedError.assign("failed to initialize plugin ").append(pluginConfigPath.string()).append(": Init failed");
+
+        checkCallTest(/* shouldHaveFailed */ true, result, error, expectedError, debugObject->initCalled);
+      }
+      cleanupSandBox(plugin);
+    }
+  }
+}
+
+SCENARIO("invoking plugin instance init", "[plugin][core]")
+{
+  std::string error;
+  PluginDebugObject *debugObject = nullptr;
+  void *ih                       = nullptr; // Instance handler pointer.
+
+  /* a sample test set of parameters */
+  static const char *args[] = {"arg1", "arg2", "arg3"};
+  static char **ARGV        = const_cast<char **>(args);
+  static char ARGC          = sizeof ARGV;
+
+  GIVEN("an instance init function")
+  {
+    fs::path pluginConfigPath   = fs::path("plugin_testing_calls.so");
+    RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+    bool result = loadPlugin(plugin, error, debugObject);
+    CHECK(true == result);
+
+    WHEN("instance init succeeds")
+    {
+      prepCallTest(/* toFail */ false, debugObject);
+      debugObject->input_ih = INSTANCE_HANDLER; /* this is what the plugin instance init will return */
+
+      result = plugin->initInstance(ARGC, ARGV, &ih, error);
+
+      THEN("expect init to be called successfully with no error and expected instance handler")
+      {
+        std::string expectedError;
+
+        checkCallTest(/* shouldHaveFailed */ false, result, error, expectedError, debugObject->initInstanceCalled);
+
+        /* Verify expected handler */
+        CHECK(INSTANCE_HANDLER == ih);
+        /* Plugin received the parameters that we passed */
+        CHECK(ARGC == debugObject->argc);
+        CHECK(ARGV == debugObject->argv);
+        for (int i = 0; i < 3; i++) {
+          CHECK(0 == strcmp(ARGV[i], debugObject->argv[i]));
+        }
+      }
+      cleanupSandBox(plugin);
+    }
+
+    WHEN("instance init fails")
+    {
+      prepCallTest(/* toFail */ true, debugObject);
+
+      result = plugin->initInstance(ARGC, ARGV, &ih, error);
+
+      THEN("expect init to be called but failed with expected error and no instance handler")
+      {
+        std::string expectedError;
+        expectedError.assign("failed to create instance for plugin ").append(pluginConfigPath.string()).append(": Init failed");
+
+        checkCallTest(/* shouldHaveFailed */ true, result, error, expectedError, debugObject->initInstanceCalled);
+
+        /* Ideally instance handler should not be touched in case of failure */
+        CHECK(nullptr == ih);
+        /* Plugin received the parameters that we passed */
+        CHECK(ARGC == debugObject->argc);
+        CHECK(ARGV == debugObject->argv);
+        for (int i = 0; i < 3; i++) {
+          CHECK(0 == strcmp(ARGV[i], debugObject->argv[i]));
+        }
+      }
+      cleanupSandBox(plugin);
+    }
+  }
+}
+
+SCENARIO("unloading the plugin", "[plugin][core]")
+{
+  std::string error;
+  PluginDebugObject *debugObject = nullptr;
+
+  GIVEN("a 'done' function")
+  {
+    fs::path pluginConfigPath   = fs::path("plugin_testing_calls.so");
+    RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+    bool result = loadPlugin(plugin, error, debugObject);
+    CHECK(true == result);
+
+    WHEN("'done' is called")
+    {
+      debugObject->clear();
+
+      plugin->done();
+
+      THEN("expect it to run") { CHECK(1 == debugObject->doneCalled); }
+      cleanupSandBox(plugin);
+    }
+  }
+
+  GIVEN("a 'delete_instance' function")
+  {
+    fs::path pluginConfigPath   = fs::path("plugin_testing_calls.so");
+    RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+    bool result = loadPlugin(plugin, error, debugObject);
+    CHECK(true == result);
+
+    WHEN("'delete_instance' is called")
+    {
+      debugObject->clear();
+
+      plugin->doneInstance(INSTANCE_HANDLER);
+
+      THEN("expect it to run and receive the right instance handler")
+      {
+        CHECK(1 == debugObject->deleteInstanceCalled);
+        CHECK(INSTANCE_HANDLER == debugObject->ih);
+      }
+      cleanupSandBox(plugin);
+    }
+  }
+}
+
+SCENARIO("config reload", "[plugin][core]")
+{
+  std::string error;
+  PluginDebugObject *debugObject = nullptr;
+
+  GIVEN("a 'config reload' callback function")
+  {
+    fs::path pluginConfigPath   = fs::path("plugin_testing_calls.so");
+    RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+    bool result = loadPlugin(plugin, error, debugObject);
+    CHECK(true == result);
+
+    WHEN("'config reload' is called")
+    {
+      debugObject->clear();
+
+      plugin->indicateReload();
+
+      THEN("expect it to run") { CHECK(1 == debugObject->reloadConfigCalled); }
+      cleanupSandBox(plugin);
+    }
+  }
+}
diff --git a/src/traffic_server/InkAPI.cc b/src/traffic_server/InkAPI.cc
index 9414b1d..d4d6916 100644
--- a/src/traffic_server/InkAPI.cc
+++ b/src/traffic_server/InkAPI.cc
@@ -1001,6 +1001,7 @@ INKContInternal::INKContInternal()
     m_closed(1),
     m_deletable(0),
     m_deleted(0),
+    m_context(0),
     m_free_magic(INKCONT_INTERN_MAGIC_ALIVE)
 {
 }
@@ -1013,18 +1014,20 @@ INKContInternal::INKContInternal(TSEventFunc funcp, TSMutex mutexp)
     m_closed(1),
     m_deletable(0),
     m_deleted(0),
+    m_context(0),
     m_free_magic(INKCONT_INTERN_MAGIC_ALIVE)
 {
   SET_HANDLER(&INKContInternal::handle_event);
 }
 
 void
-INKContInternal::init(TSEventFunc funcp, TSMutex mutexp)
+INKContInternal::init(TSEventFunc funcp, TSMutex mutexp, void *context)
 {
   SET_HANDLER(&INKContInternal::handle_event);
 
   mutex        = (ProxyMutex *)mutexp;
   m_event_func = funcp;
+  m_context    = context;
 }
 
 void
@@ -1095,7 +1098,11 @@ INKContInternal::handle_event(int event, void *edata)
       Debug("plugin", "INKCont Deletable but not deleted %d", m_event_count);
     }
   } else {
-    int retval = m_event_func((TSCont)this, (TSEvent)event, edata);
+    /* set the plugin context */
+    auto *previousContext = pluginThreadContext;
+    pluginThreadContext   = reinterpret_cast<PluginThreadContext *>(m_context);
+    int retval            = m_event_func((TSCont)this, (TSEvent)event, edata);
+    pluginThreadContext   = previousContext;
     if (edata && event == EVENT_INTERVAL) {
       Event *e = reinterpret_cast<Event *>(edata);
       if (e->period != 0) {
@@ -4371,6 +4378,8 @@ TSMgmtSourceGet(const char *var_name, TSMgmtSource *source)
 //
 ////////////////////////////////////////////////////////////////////
 
+extern thread_local PluginThreadContext *pluginThreadContext;
+
 TSCont
 TSContCreate(TSEventFunc funcp, TSMutex mutexp)
 {
@@ -4379,9 +4388,13 @@ TSContCreate(TSEventFunc funcp, TSMutex mutexp)
     sdk_assert(sdk_sanity_check_mutex(mutexp) == TS_SUCCESS);
   }
 
+  if (pluginThreadContext) {
+    pluginThreadContext->acquire();
+  }
+
   INKContInternal *i = INKContAllocator.alloc();
 
-  i->init(funcp, mutexp);
+  i->init(funcp, mutexp, pluginThreadContext);
   return (TSCont)i;
 }
 
@@ -4392,6 +4405,10 @@ TSContDestroy(TSCont contp)
 
   INKContInternal *i = (INKContInternal *)contp;
 
+  if (i->m_context) {
+    reinterpret_cast<PluginThreadContext *>(i->m_context)->release();
+  }
+
   i->destroy();
 }
 
@@ -5990,6 +6007,8 @@ TSHttpTxnReenable(TSHttpTxn txnp, TSEvent event)
   }
 }
 
+TSReturnCode TSHttpArgIndexNameLookup(UserArg::Type type, const char *name, int *arg_idx, const char **description);
+
 TSReturnCode
 TSHttpArgIndexReserve(UserArg::Type type, const char *name, const char *description, int *ptr_idx)
 {
@@ -5997,7 +6016,19 @@ TSHttpArgIndexReserve(UserArg::Type type, const char *name, const char *descript
   sdk_assert(sdk_sanity_check_null_ptr(name) == TS_SUCCESS);
   sdk_assert(0 <= type && type < UserArg::Type::COUNT);
 
-  int idx   = UserArgIdx[type]++;
+  int idx;
+
+  /* Since this function is meant to be called during plugin initialization we could end up "leaking" indices during plugins reload.
+   * Make sure we allocate 1 index per name, also current TSHttpArgIndexNameLookup() implementation assumes 1-1 relationship as
+   * well. */
+  const char *desc;
+  if (TS_SUCCESS == TSHttpArgIndexNameLookup(type, name, &idx, &desc)) {
+    // Found existing index.
+    *ptr_idx = idx;
+    return TS_SUCCESS;
+  }
+
+  idx       = UserArgIdx[type]++;
   int limit = (type == UserArg::Type::VCONN) ? TS_VCONN_MAX_USER_ARG : TS_HTTP_MAX_USER_ARG;
 
   if (idx < limit) {
@@ -6648,11 +6679,15 @@ TSVConnCreate(TSEventFunc event_funcp, TSMutex mutexp)
   // TODO: probably don't need this if memory allocations fails properly
   sdk_assert(sdk_sanity_check_mutex(mutexp) == TS_SUCCESS);
 
+  if (pluginThreadContext) {
+    pluginThreadContext->acquire();
+  }
+
   INKVConnInternal *i = INKVConnAllocator.alloc();
 
   sdk_assert(sdk_sanity_check_null_ptr((void *)i) == TS_SUCCESS);
 
-  i->init(event_funcp, mutexp);
+  i->init(event_funcp, mutexp, pluginThreadContext);
   return reinterpret_cast<TSVConn>(i);
 }
 
diff --git a/src/tscore/ts_file.cc b/src/tscore/ts_file.cc
index 3467176..d50fae3 100644
--- a/src/tscore/ts_file.cc
+++ b/src/tscore/ts_file.cc
@@ -20,6 +20,8 @@
 
 #include "tscore/ts_file.h"
 #include <fcntl.h>
+#include <sys/types.h>
+#include <dirent.h>
 
 namespace ts
 {
@@ -56,12 +58,220 @@ namespace file
     return zret;
   }
 
+  path
+  temp_directory_path()
+  {
+    /* ISO/IEC 9945 (POSIX): The path supplied by the first environment variable found in the list TMPDIR, TMP, TEMP, TEMPDIR.
+     * If none of these are found, "/tmp" */
+    char const *folder = nullptr;
+    if ((nullptr == (folder = getenv("TMPDIR"))) && (nullptr == (folder = getenv("TMP"))) &&
+        (nullptr == (folder = getenv("TEMPDIR")))) {
+      folder = "/tmp";
+    }
+    return path(folder);
+  }
+
+  path
+  current_path()
+  {
+    char cwd[PATH_MAX];
+    if (::getcwd(cwd, sizeof(cwd)) != NULL) {
+      return path(cwd);
+    }
+    return path();
+  }
+
+  path
+  canonical(const path &p, std::error_code &ec)
+  {
+    if (p.empty()) {
+      ec = std::error_code(EINVAL, std::system_category());
+      return path();
+    }
+
+    char buf[PATH_MAX + 1];
+    char *res = ::realpath(p.c_str(), buf);
+    if (res) {
+      ec = std::error_code();
+      return path(res);
+    }
+
+    ec = std::error_code(errno, std::system_category());
+    return path();
+  }
+
+  bool
+  exists(const path &p)
+  {
+    std::error_code ec;
+    status(p, ec);
+    return !(ec && ENOENT == ec.value());
+  }
+
+  static bool
+  do_mkdir(const path &p, std::error_code &ec, mode_t mode)
+  {
+    struct stat st;
+    if (stat(p.c_str(), &st) != 0) {
+      if (mkdir(p.c_str(), mode) != 0 && errno != EEXIST) {
+        ec = std::error_code(errno, std::system_category());
+        return false;
+      }
+    } else if (!S_ISDIR(st.st_mode)) {
+      ec = std::error_code(ENOTDIR, std::system_category());
+      return false;
+    }
+    return true;
+  }
+
+  bool
+  create_directories(const path &p, std::error_code &ec, mode_t mode) noexcept
+  {
+    if (p.empty()) {
+      ec = std::error_code(EINVAL, std::system_category());
+      return false;
+    }
+
+    bool result = false;
+    ec          = std::error_code();
+
+    size_t pos = 0;
+    std::string token;
+    while ((pos = p.string().find_first_of(p.preferred_separator, pos)) != std::string::npos) {
+      token = p.string().substr(0, pos);
+      if (!token.empty()) {
+        result = do_mkdir(path(token), ec, mode);
+      }
+      pos = pos + sizeof(p.preferred_separator);
+    }
+
+    if (result) {
+      result = do_mkdir(p, ec, mode);
+    }
+    return result;
+  }
+
+  bool
+  copy(const path &from, const path &to, std::error_code &ec)
+  {
+    static int BUF_SIZE = 65536;
+    FILE *src, *dst;
+    size_t in, out;
+    char buf[BUF_SIZE];
+    int bufsize = BUF_SIZE;
+
+    if (from.empty() || to.empty()) {
+      ec = std::error_code(EINVAL, std::system_category());
+      return false;
+    }
+
+    ec = std::error_code();
+
+    std::error_code err;
+    path final_to;
+    file_status s = status(to, err);
+    if (!(err && ENOENT == err.value()) && is_dir(s)) {
+      const size_t last_slash_idx = from.string().find_last_of(from.preferred_separator);
+      std::string filename        = from.string().substr(last_slash_idx + 1);
+      final_to                    = to / filename;
+    } else {
+      final_to = to;
+    }
+
+    if (nullptr == (src = fopen(from.c_str(), "r"))) {
+      ec = std::error_code(errno, std::system_category());
+      return false;
+    }
+    if (nullptr == (dst = fopen(final_to.c_str(), "w"))) {
+      ec = std::error_code(errno, std::system_category());
+      return false;
+    }
+
+    while (1) {
+      in = fread(buf, 1, bufsize, src);
+      if (0 == in)
+        break;
+      out = fwrite(buf, 1, in, dst);
+      if (0 == out)
+        break;
+    }
+
+    fclose(src);
+    fclose(dst);
+
+    return true;
+  }
+
+  static bool
+  remove_path(const path &p, std::error_code &ec)
+  {
+    DIR *dir;
+    struct dirent *entry;
+    bool res = true;
+    std::error_code err;
+
+    file_status s = status(p, err);
+    if (err && ENOENT == err.value()) {
+      // file/dir does not exist
+      return false;
+    } else if (is_regular_file(s)) {
+      // regular file, try to remove it!
+      if (unlink(p.c_str()) != 0) {
+        ec  = std::error_code(errno, std::system_category());
+        res = false;
+      }
+      return res;
+    } else if (!is_dir(s)) {
+      // not a directory
+      ec = std::error_code(ENOTDIR, std::system_category());
+      return false;
+    }
+
+    // recursively remove nested files and directories
+    if (nullptr == (dir = opendir(p.c_str()))) {
+      ec = std::error_code(errno, std::system_category());
+      return false;
+    }
+
+    while (nullptr != (entry = readdir(dir))) {
+      if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) {
+        continue;
+      }
+
+      remove_path(p / entry->d_name, ec);
+    }
+
+    if (0 != rmdir(p.c_str())) {
+      ec = std::error_code(errno, std::system_category());
+    }
+
+    closedir(dir);
+    return true;
+  }
+
+  bool
+  remove(const path &p, std::error_code &ec)
+  {
+    if (p.empty()) {
+      ec = std::error_code(EINVAL, std::system_category());
+      return false;
+    }
+
+    ec = std::error_code();
+    return remove_path(p, ec);
+  } // namespace file
+
   int
   file_type(const file_status &fs)
   {
     return fs._stat.st_mode & S_IFMT;
   }
 
+  time_t
+  modification_time(const file_status &fs)
+  {
+    return fs._stat.st_mtime;
+  }
   uintmax_t
   file_size(const file_status &fs)
   {
diff --git a/src/tscore/unit_tests/test_ts_file.cc b/src/tscore/unit_tests/test_ts_file.cc
index 58366de..603ded3 100644
--- a/src/tscore/unit_tests/test_ts_file.cc
+++ b/src/tscore/unit_tests/test_ts_file.cc
@@ -22,6 +22,7 @@
 */
 
 #include <iostream>
+#include <fstream> /* ofstream */
 
 #include "tscore/ts_file.h"
 #include "../../../tests/include/catch.hpp"
@@ -67,3 +68,195 @@ TEST_CASE("ts_file_io", "[libts][ts_file_io]")
   REQUIRE(ec.value() == 2);
   REQUIRE(ts::file::is_readable(file) == false);
 }
+
+TEST_CASE("ts_file::path::parent_path", "[libts][fs_file]")
+{
+  CHECK(ts::file::path("/").parent_path() == path("/"));
+  CHECK(ts::file::path("/absolute/path/file.txt").parent_path() == ts::file::path("/absolute/path"));
+  CHECK(ts::file::path("/absolute/path/.").parent_path() == ts::file::path("/absolute/path"));
+
+  CHECK(ts::file::path("relative/path/file.txt").parent_path() == ts::file::path("relative/path"));
+  CHECK(ts::file::path("relative/path/.").parent_path() == ts::file::path("relative/path"));
+  CHECK(ts::file::path(".").parent_path() == ts::file::path(""));
+}
+
+static std::string
+setenvvar(const std::string &name, const std::string &value)
+{
+  std::string saved;
+  if (nullptr != getenv(name.c_str())) {
+    saved.assign(value);
+  }
+
+  if (!value.empty()) {
+    setenv(name.c_str(), value.c_str(), 1);
+  } else {
+    unsetenv(name.c_str());
+  }
+
+  return saved;
+}
+
+TEST_CASE("ts_file::path::temp_directory_path", "[libts][fs_file]")
+{
+  // Clean all temp dir env variables.
+  std::string s1 = setenvvar("TMPDIR", std::string());
+  std::string s2 = setenvvar("TEMPDIR", std::string());
+  std::string s3 = setenvvar("TMP", std::string());
+  std::string s;
+
+  // If nothing defined return "/tmp"
+  CHECK(ts::file::temp_directory_path() == ts::file::path("/tmp"));
+
+  // TMPDIR defined.
+  s = setenvvar("TMPDIR", "/temp_dirname1");
+  CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname1"));
+  setenvvar("TMPDIR", s);
+
+  // TEMPDIR
+  s = setenvvar("TEMPDIR", "/temp_dirname");
+  CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname"));
+  // TMP defined, it should take precedence over TEMPDIR.
+  s = setenvvar("TMP", "/temp_dirname1");
+  CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname1"));
+  // TMPDIR defined, it should take precedence over TMP.
+  s = setenvvar("TMPDIR", "/temp_dirname2");
+  CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname2"));
+  setenvvar("TMPDIR", s);
+  setenvvar("TMP", s);
+  setenvvar("TEMPDIR", s);
+
+  // Restore all temp dir env variables to their previous state.
+  setenvvar("TMPDIR", s1);
+  setenvvar("TEMPDIR", s2);
+  setenvvar("TMP", s3);
+}
+
+TEST_CASE("ts_file::path::create_directories", "[libts][fs_file]")
+{
+  std::error_code ec;
+  path tempdir = ts::file::temp_directory_path();
+
+  CHECK_FALSE(ts::file::create_directories(path(), ec));
+  CHECK(ec.value() == EINVAL);
+
+  path testdir1 = tempdir / "dir1";
+  CHECK(ts::file::create_directories(testdir1, ec));
+  CHECK(ts::file::exists(testdir1));
+
+  path testdir2 = testdir1 / "dir2";
+  CHECK(ts::file::create_directories(testdir1, ec));
+  CHECK(ts::file::exists(testdir1));
+
+  // Cleanup
+  CHECK(ts::file::remove(testdir1, ec));
+  CHECK_FALSE(ts::file::exists(testdir1));
+}
+
+TEST_CASE("ts_file::path::remove", "[libts][fs_file]")
+{
+  std::error_code ec;
+  path tempdir = ts::file::temp_directory_path();
+
+  CHECK_FALSE(ts::file::remove(path(), ec));
+  CHECK(ec.value() == EINVAL);
+
+  path testdir1 = tempdir / "dir1";
+  path testdir2 = testdir1 / "dir2";
+  path file1    = testdir2 / "test.txt";
+
+  // Simple creation and removal of a directory /tmp/dir1
+  CHECK(ts::file::create_directories(testdir1, ec));
+  CHECK(ts::file::exists(testdir1));
+  CHECK(ts::file::remove(testdir1, ec));
+  CHECK_FALSE(ts::file::exists(testdir1));
+
+  // Create /tmp/dir1/dir2 and remove /tmp/dir1/dir2 => /tmp/dir1 should exist
+  CHECK(ts::file::create_directories(testdir2, ec));
+  CHECK(ts::file::remove(testdir2, ec));
+  CHECK(ts::file::exists(testdir1));
+
+  // Create a file, remove it, test if exists and then attempting to remove it again should fail.
+  CHECK(ts::file::create_directories(testdir2, ec));
+  std::ofstream file(file1.string());
+  file << "Simple test file";
+  file.close();
+  CHECK(ts::file::exists(file1));
+  CHECK(ts::file::remove(file1, ec));
+  CHECK_FALSE(ts::file::exists(file1));
+  CHECK_FALSE(ts::file::remove(file1, ec));
+
+  // Clean up.
+  CHECK(ts::file::remove(testdir1, ec));
+  CHECK_FALSE(ts::file::exists(testdir1));
+}
+
+TEST_CASE("ts_file::path::canonical", "[libts][fs_file]")
+{
+  std::error_code ec;
+  path tempdir    = ts::file::canonical(ts::file::temp_directory_path(), ec);
+  path testdir1   = tempdir / "dir1";
+  path testdir2   = testdir1 / "dir2";
+  path testdir3   = testdir2 / "dir3";
+  path unorthodox = testdir3 / path("..") / path("..") / "dir2";
+
+  // Invalid empty path.
+  CHECK(path() == ts::file::canonical(path(), ec));
+  CHECK(ec.value() == EINVAL);
+
+  // Fail if directory does not exist
+  CHECK(path() == ts::file::canonical(unorthodox, ec));
+  CHECK(ec.value() == ENOENT);
+
+  // Create the dir3 and test again
+  CHECK(create_directories(testdir3, ec));
+  CHECK(ts::file::exists(testdir3));
+  CHECK(ts::file::exists(testdir2));
+  CHECK(ts::file::exists(testdir1));
+  CHECK(ts::file::exists(unorthodox));
+  CHECK(ts::file::canonical(unorthodox, ec) == testdir2);
+  CHECK(ec.value() == 0);
+
+  // Cleanup
+  CHECK(ts::file::remove(testdir1, ec));
+  CHECK_FALSE(ts::file::exists(testdir1));
+}
+
+TEST_CASE("ts_file::path::copy", "[libts][fs_file]")
+{
+  std::error_code ec;
+  path tempdir  = ts::file::temp_directory_path();
+  path testdir1 = tempdir / "dir1";
+  path testdir2 = testdir1 / "dir2";
+  path file1    = testdir2 / "test1.txt";
+  path file2    = testdir2 / "test2.txt";
+
+  // Invalid empty path, both to and from parameters.
+  CHECK_FALSE(ts::file::copy(path(), path(), ec));
+  CHECK(ec.value() == EINVAL);
+
+  CHECK(ts::file::create_directories(testdir2, ec));
+  std::ofstream file(file1.string());
+  file << "Simple test file";
+  file.close();
+  CHECK(ts::file::exists(file1));
+
+  // Invalid empty path, now from parameter is ok but to is empty
+  CHECK_FALSE(ts::file::copy(file1, path(), ec));
+  CHECK(ec.value() == EINVAL);
+
+  // successfull copy: "to" is directory
+  CHECK(ts::file::copy(file1, testdir2, ec));
+  CHECK(ec.value() == 0);
+
+  // successful copy: "to" is file
+  CHECK(ts::file::copy(file1, file2, ec));
+  CHECK(ec.value() == 0);
+
+  // Compare the content
+  CHECK(ts::file::load(file1, ec) == ts::file::load(file2, ec));
+
+  // Cleanup
+  CHECK(ts::file::remove(testdir1, ec));
+  CHECK_FALSE(ts::file::exists(testdir1));
+}
\ No newline at end of file