Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1170d4e
Make it so that links have their own tag pool separate from interfaces
Ktmi Apr 22, 2025
43df4db
Prematurely *optimized* tag_ranges operators
Ktmi Apr 24, 2025
3cd5c19
Add `default_tag_ranges` to `TAGCapable` slots
Ktmi Apr 28, 2025
0d45bff
Merge branch 'master' into feature/tag_capable
Ktmi Jun 12, 2025
7c9bc61
Made it so that special tags wont accidentally get invalidated by rem…
Ktmi Jun 27, 2025
6985ab7
Fixed setting default_tag_ranges setting the wrong values
Ktmi Jul 2, 2025
53254ce
Merge remote-tracking branch 'origin/premature_optimization/tag_range…
Ktmi Jul 2, 2025
0c6efaf
Fixes for tag_ranges + additional tests
Ktmi Jul 22, 2025
e2e3033
Merge branch 'master' into feature/tag_capable
Ktmi Jul 22, 2025
72d5cb7
Add locks to generic entities and controller + more type info
Ktmi Aug 12, 2025
64d445e
Add lock mocks for tests
Ktmi Aug 13, 2025
dd1c7f4
Fix validating against default when default is being udpated
Ktmi Aug 14, 2025
5f96029
Add atomic operations to tag_capable
Ktmi Aug 20, 2025
1d83144
Removed test for removed code segment.
Ktmi Aug 21, 2025
998bde8
Make supported_tag_types explicit
Ktmi Sep 4, 2025
b9d4ce2
Allow empty tag ranges
Ktmi Sep 11, 2025
c122c56
Reduce locks
Ktmi Nov 13, 2025
86b1ab9
Linter cleanup
Ktmi Dec 1, 2025
1b3b120
Update TAGCapable docstring
Ktmi Jan 7, 2026
9b5ee4d
Fix `all_tags_available` not properly checking `special_tags`
Ktmi Jan 7, 2026
b746863
Updated tests
Ktmi Jan 14, 2026
533ce82
Additional tests for tag_ranges
Ktmi Jan 14, 2026
4de08ab
Apply wraps decorator to atomic operations in tag_capable
Ktmi Jan 15, 2026
aba18a3
Use kwargs when initializing TAGCapable
Ktmi Jan 15, 2026
9dca18d
Merge branch 'master' into feature/tag_capable
Ktmi Jan 15, 2026
a83382e
Updated changelog
Ktmi Jan 15, 2026
44c96b2
Revert some tag_ranges changes
Ktmi Jan 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Changed
- ``controller`` now holds the dictionary of ``links`` and it can be accessed by other NApps by calling ``self.controller.links``.
- Each ``Link`` now has a ``threading.Lock`` to perform any change or check on its attributes.
- Kytos shutdown will now wait for every NApp to shutdown completely.
- ``Links`` now have a separate tag pool from ``Interfaces``.
- ``Interfaces`` should now use the lock of their ``Switch`` to maintain consistency.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ktmi we also need to make it explicit here that's a breaking changing and NApps who used to use the related interface old tags method need to refactor as you've done for our core NApps, and then in the release notes you just mention that's expected for developers to refactor if they have other non core NApps.

Note: it's probably not worth trying to maintain deprecation compatibility due to extra effort it'd require in terms of code maintenance.


Fixed
=====
Expand All @@ -19,6 +21,7 @@ Fixed
Added
=====
- Added headers to NApp file response which tells browsers not to cache them.
- Added ``TAGCapable``, a new mixin for providing tag functionality for interfaces and links.

[2025.1.0] - 2025-04-15
***********************
Expand Down
2 changes: 2 additions & 0 deletions kytos/core/common.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Module with common classes for the controller."""
from enum import Enum
from threading import Lock

from kytos.core.config import KytosConfig

Expand All @@ -21,6 +22,7 @@ def __init__(self):
"""Create the GenericEntity object with empty metadata dictionary."""
options = KytosConfig().options['daemon']
self.metadata = {}
self.lock = Lock()

self._active: bool = True
self._enabled: bool = options.enable_entities_by_default
Expand Down
126 changes: 58 additions & 68 deletions kytos/core/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from importlib import reload as reload_module
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
from typing import Iterable, Optional
from typing import Optional

from pyof.foundation.exceptions import PackException

Expand Down Expand Up @@ -134,7 +134,7 @@ def __init__(self, options=None, loop: AbstractEventLoop = None):
#:
#: The key is the switch dpid, while the value is a Switch object.
self.switches: dict[str, Switch] = {}
self._switches_lock = threading.Lock()
self.switches_lock = threading.Lock()

#: datetime.datetime: Time when the controller finished starting.
self.started_at = None
Expand Down Expand Up @@ -173,7 +173,6 @@ def __init__(self, options=None, loop: AbstractEventLoop = None):
self.pacer = Pacer("memory://")

self.links: dict[str, Link] = {}
self.links_lock = threading.Lock()
Link.register_status_reason_func("controller_mismatched_reason",
self.detect_mismatched_link)
Link.register_status_func("controller_mismatched_status",
Expand Down Expand Up @@ -742,7 +741,7 @@ def get_switch_or_create(self, dpid, connection=None):
:class:`~kytos.core.switch.Switch`: new or existent switch.

"""
with self._switches_lock:
with self.switches_lock:
if connection:
self.create_or_update_connection(connection)

Expand Down Expand Up @@ -1057,55 +1056,62 @@ def get_link_or_create(
Returns:
Tuple(Link, bool): Link and a boolean whether it has been created.
"""
with self.links_lock:
new_link = Link(endpoint_a, endpoint_b)

# If new_link is an old link but mismatched,
# then treat it as a new link
if (new_link.id in self.links
and not self.detect_mismatched_link(new_link)):
return (self.links[new_link.id], False)

with new_link.link_lock:
# Check if any interface already has a link
# This old_link is a leftover link that needs to be removed
# The other endpoint of the link is the leftover interface
if endpoint_a.link and endpoint_a.link != new_link:
old_link = endpoint_a.link
leftover_interface = (old_link.endpoint_a
if old_link.endpoint_a != endpoint_a
else old_link.endpoint_b)
self.log.warning(f"Leftover mismatched link"
f" {endpoint_a.link} in interface"
f" {leftover_interface}")

if endpoint_b.link and endpoint_b.link != new_link:
old_link = endpoint_b.link
leftover_interface = (old_link.endpoint_b
if old_link.endpoint_b != endpoint_b
else old_link.endpoint_a)
self.log.warning(f"Leftover mismatched link "
f" {endpoint_b.link} in interface"
f" {leftover_interface}")

if new_link.id not in self.links:
self.links[new_link.id] = new_link

endpoint_a.update_link(new_link)
endpoint_b.update_link(new_link)
new_link.endpoint_a = endpoint_a
new_link.endpoint_b = endpoint_b
endpoint_a.nni = True
endpoint_b.nni = True

if link_dict:
if link_dict['enabled']:
new_link.enable()
else:
new_link.disable()
new_link = Link(endpoint_a, endpoint_b)

# If new_link is an old link but mismatched,
# then treat it as a new link
if (new_link.id in self.links
and not self.detect_mismatched_link(new_link)):
return (self.links[new_link.id], False)

with new_link.lock:
# Check if any interface already has a link
# This old_link is a leftover link that needs to be removed
# The other endpoint of the link is the leftover interface
if endpoint_a.link and endpoint_a.link != new_link:
old_link = endpoint_a.link
leftover_interface = (
old_link.endpoint_a
if old_link.endpoint_a != endpoint_a
else old_link.endpoint_b
)
self.log.warning(
f"Leftover mismatched link"
f" {endpoint_a.link} in interface"
f" {leftover_interface}"
)

if endpoint_b.link and endpoint_b.link != new_link:
old_link = endpoint_b.link
leftover_interface = (
old_link.endpoint_b
if old_link.endpoint_b != endpoint_b
else old_link.endpoint_a
)
self.log.warning(
f"Leftover mismatched link "
f" {endpoint_b.link} in interface"
f" {leftover_interface}"
)

if link_dict.get("metadata"):
new_link.extend_metadata(link_dict["metadata"])
if new_link.id not in self.links:
self.links[new_link.id] = new_link

endpoint_a.update_link(new_link)
endpoint_b.update_link(new_link)
new_link.endpoint_a = endpoint_a
new_link.endpoint_b = endpoint_b
endpoint_a.nni = True
endpoint_b.nni = True

if link_dict:
if link_dict['enabled']:
new_link.enable()
else:
new_link.disable()

if link_dict.get("metadata"):
new_link.extend_metadata(link_dict["metadata"])

return (self.links[new_link.id], True)

Expand All @@ -1117,22 +1123,6 @@ def get_link(self, link_id: str) -> Optional[Link]:
"""
return self.links.get(link_id)

def get_links_from_interfaces(
self,
interfaces: Iterable[Interface]
) -> dict[str, Link]:
"""Get a list of links that matched to all/any given interfaces."""
links_found = {}
with self.links_lock:
for link in self.links.copy().values():
for interface in interfaces:
if any((
interface.id == link.endpoint_a.id,
interface.id == link.endpoint_b.id,
)):
links_found[link.id] = link
return links_found

@staticmethod
def detect_mismatched_link(link: Link) -> frozenset[str]:
"""Check if a link is mismatched."""
Expand Down
Loading