FreshPorts uses a cache for every port page (e.g. https://www.freshports.org/sysutils/bacula-server/). When an update to a port occurs, we must clear the cache for that port. This sounds simple, and it is. The various ways in which updates occur complicates the situation.
This post is all about fixing one edge case: the addition or removal of a port dependency (e.g. RUN_DEPENDS, LIB_DEPENDS).
When port A depends on port B, this fact is listed on both pages, but adding/removing a dependency is not properly handled (see this bug). Tonight, I have coded a fix. I’d like it to be less complex, but it has been fixed.
The table
This is the table in question:
freshports.org=# \d port_dependencies
Table "public.port_dependencies"
Column | Type | Modifiers
------------------------+--------------+-----------
port_id | integer | not null
port_id_dependent_upon | integer | not null
dependency_type | character(1) | not null
Indexes:
"port_dependencies_pkey" PRIMARY KEY, btree (port_id, port_id_dependent_upon, dependency_type)
Foreign-key constraints:
"port_dependencies_port_id_dependent_upon_fkey" FOREIGN KEY (port_id_dependent_upon) REFERENCES ports(id) ON UPDATE CASCADE ON DELETE CASCADE
"port_dependencies_port_id_fkey" FOREIGN KEY (port_id) REFERENCES ports(id) ON UPDATE CASCADE ON DELETE CASCADE
freshports.org=#
Using the terminology from the first sentence, port A and port B are represented by port_id and port_id_dependent_upon respectively. Any new row or any deleted row must invalidate the cache for both port A and port B.
Keeping track of cache to clear
This is the table we use to clear the ports cache:
freshports.org=# \d cache_clearing_ports
Table "public.cache_clearing_ports"
Column | Type | Modifiers
------------+-----------------------------+-------------------------------------------------------------------
id | integer | not null default nextval('cache_clearing_ports_id_seq'::regclass)
port_id | integer | not null
category | text | not null
port | text | not null
date_added | timestamp without time zone | not null default now()
Indexes:
"cache_clearing_ports_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"cache_clearing_ports_port_id_fkey" FOREIGN KEY (port_id) REFERENCES ports(id) ON UPDATE CASCADE ON DELETE CASCADE
freshports.org=#
We add rows to this table to indicate that a given port must be cleared from the cache.
On INSERT
On an insert to the port_dependencies table, let’s do this:
CREATE OR REPLACE FUNCTION port_dependencies_insert_clear_cache() RETURNS TRIGGER AS $$
DECLARE
l_cache_clearing_ports_id int8;
l_port text;
l_category text;
BEGIN
--
-- This function handles the addition of a new dependency.
-- yes, we need to clear the cache for both port_id and port_id_dependent_upon
-- from the cache_clearing_ports. I figure there is a shorter way to this but
-- I cannot think it through right now.
--
IF TG_OP = 'INSERT' THEN
-- handle port A (port_id)
SELECT port_id
INTO l_cache_clearing_ports_id
FROM cache_clearing_ports
WHERE port_id = NEW.port_id;
IF NOT FOUND THEN
SELECT category, name
INTO l_category, l_port
FROM ports_all
WHERE id = NEW.port_id;
INSERT INTO cache_clearing_ports (port_id, category, port)
VALUES (NEW.port_id, l_category, l_port);
END IF;
-- handle port B (port_id_dependent_upon)
SELECT port_id
INTO l_cache_clearing_ports_id
FROM cache_clearing_ports
WHERE port_id = NEW.port_id_dependent_upon;
IF NOT FOUND THEN
SELECT category, name
INTO l_category, l_port
FROM ports_all
WHERE id = NEW.port_id_dependent_upon;
INSERT INTO cache_clearing_ports (port_id, category, port)
VALUES (NEW.port_id_dependent_upon, l_category, l_port);
END IF;
NOTIFY port_updated;
END IF;
-- when a port changes, add an entry to the cache clearing table
RETURN NEW;
END
$$ LANGUAGE 'plpgsql';
DROP TRIGGER IF EXISTS port_dependencies_insert_clear_cache ON port_dependencies;
CREATE TRIGGER port_dependencies_insert_clear_cache
AFTER INSERT on port_dependencies
FOR EACH ROW
EXECUTE PROCEDURE port_dependencies_insert_clear_cache();
That function is rather long, and I know it can be simplified, but that’s not on tonight’s agenda. If you are so inclined, I am happy to hear your suggestions. Specifically, I think I can do an INSERT INTO … WHERE NOT EXISTS and do the SELECT & INSERT in one step, without the need for IF FOUND.
On DELETE
Here is the code for the DELETE.
CREATE OR REPLACE FUNCTION port_dependencies_delete_clear_cache() RETURNS TRIGGER AS $$
DECLARE
l_cache_clearing_ports_id int8;
l_port text;
l_category text;
BEGIN
--
-- This function handles the deletion of a existing dependency.
-- yes, we need to clear the cache for both port_id and port_id_dependent_upon
-- from the cache_clearing_ports. I figure there is a shorter way to this but
-- I cannot think it through right now.
--
IF TG_OP = 'DELETE' THEN
SELECT port_id
INTO l_cache_clearing_ports_id
FROM cache_clearing_ports
WHERE port_id = OLD.port_id;
IF NOT FOUND THEN
SELECT category, name
INTO l_category, l_port
FROM ports_all
WHERE id = OLD.port_id;
INSERT INTO cache_clearing_ports (port_id, category, port)
VALUES (OLD.port_id, l_category, l_port);
END IF;
SELECT port_id
INTO l_cache_clearing_ports_id
FROM cache_clearing_ports
WHERE port_id = OLD.port_id_dependent_upon;
IF NOT FOUND THEN
SELECT category, name
INTO l_category, l_port
FROM ports_all
WHERE id = OLD.port_id_dependent_upon;
INSERT INTO cache_clearing_ports (port_id, category, port)
VALUES (OLD.port_id_dependent_upon, l_category, l_port);
END IF;
NOTIFY port_updated;
END IF;
-- when a port changes, add an entry to the cache clearing table
RETURN OLD;
END
$$ LANGUAGE 'plpgsql';
DROP TRIGGER IF EXISTS port_dependencies_delete_clear_cache ON port_dependencies;
CREATE TRIGGER port_dependencies_delete_clear_cache
AFTER DELETE on port_dependencies
FOR EACH ROW
EXECUTE PROCEDURE port_dependencies_delete_clear_cache();
It is nearly identical to the first function. Again, those of you who want to suggest improvements there, feel free.
I will run this in dev for a while and see how it goes. Initial testing has been promising.
NOTE: there are no updates to this table, by design. Instead when we see a new commit to a port, we do a DELETE from port_dependencies WHERE port_id = :a.











