Skip to content

Commit

Permalink
Merge pull request #9 from evo-company/fix-hashing-algo-different-see…
Browse files Browse the repository at this point in the history
…d-on-workers

fix hash function to work on different workers the same
  • Loading branch information
n4mespace authored Mar 21, 2024
2 parents acca6a3 + 00682c2 commit dedd3a7
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 12 deletions.
7 changes: 6 additions & 1 deletion featureflags_client/http/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Any, Callable, Dict, List, Optional, Set

from featureflags_client.http.types import Check, Flag, Operator
from featureflags_client.http.utils import hash_flag_value

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -79,7 +80,11 @@ def percent(name: str, value: Any) -> Callable:
@except_false
def proc(ctx: Dict[str, Any]) -> bool:
ctx_val = ctx.get(name, _UNDEFINED)
return ctx_val is not _UNDEFINED and hash(ctx_val) % 100 < int(value)
if ctx_val is _UNDEFINED:
return False

hash_ctx_val = hash_flag_value(name, ctx_val)
return hash_ctx_val % 100 < int(value)

return proc

Expand Down
8 changes: 8 additions & 0 deletions featureflags_client/http/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import hashlib
import inspect
import struct
from enum import Enum, EnumMeta
from typing import Any, Dict, Generator, Mapping, Type, Union

Expand Down Expand Up @@ -54,3 +56,9 @@ def intervals_gen(
else:
success = yield retry_interval
retry_interval = min(retry_interval * 2, retry_interval_max)


def hash_flag_value(name: str, value: Any) -> int:
hash_digest = hashlib.md5(f"{name}{value}".encode()).digest() # noqa: S324
(hash_int,) = struct.unpack("<L", hash_digest[-4:])
return hash_int
33 changes: 22 additions & 11 deletions featureflags_client/tests/http/test_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@
wildcard,
)
from featureflags_client.http.types import Operator
from featureflags_client.http.utils import hash_flag_value

TEST_OPERATOR_NAME = "test_operator"
TEST_VARIABLE_NAME = "test_operator"


def check_op(left: Any, op: Callable, right: Any) -> bool:
return op("var", right)({"var": left} if left is not _UNDEFINED else {})
context = {TEST_VARIABLE_NAME: left} if left is not _UNDEFINED else {}
return op(TEST_OPERATOR_NAME, right)(context)


def test_false():
Expand Down Expand Up @@ -82,24 +87,30 @@ def test_contains():


def test_percent():
assert check_op(0, percent, 1) is True
assert check_op(1, percent, 1) is False
assert check_op(1, percent, 2) is True

# If percent <= 0 return False
for i in range(-150, 150):
assert check_op(i, percent, 0) is False

# If percent >= 100 return True
for i in range(-150, 150):
assert check_op(i, percent, 100) is True

assert check_op("foo", percent, 100) is True
assert check_op("foo", percent, 0) is False
assert check_op("foo", percent, hash("foo") % 100 + 1) is True
assert check_op("foo", percent, hash("foo") % 100 - 1) is False

# Check not integer values
assert check_op(_UNDEFINED, percent, 100) is False
assert check_op(50, percent, _UNDEFINED) is False
assert check_op(_UNDEFINED, percent, _UNDEFINED) is False
assert check_op("foo", percent, "not_number") is False

# Check string values
assert check_op("foo", percent, "100") is True
assert check_op("foo", percent, "not_number") is False
assert check_op("foo", percent, "0") is False
assert check_op("foo", percent, 100) is True
assert check_op("foo", percent, 0) is False

# Check hash comparison
foo_hash = hash_flag_value(TEST_VARIABLE_NAME, "foo")
assert check_op("foo", percent, foo_hash % 100 + 1) is True
assert check_op("foo", percent, foo_hash % 100 - 1) is False


def test_regexp():
Expand Down

0 comments on commit dedd3a7

Please sign in to comment.