mirror of
https://github.com/Telecominfraproject/wlan-cloud-rrm.git
synced 2025-10-30 18:17:58 +00:00
Pass down RRM algorithm parameters (#61)
This commit is contained in:
@@ -11,6 +11,8 @@ selected channel. This is only for testing and re-initialization.
|
|||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* `mode`: "random"
|
* `mode`: "random"
|
||||||
|
* `setDifferentChannelPerAp`: If true, will set a different random channel per AP. If false, it will set the same random channel for all APs.
|
||||||
|
* values: `true`, `false` (default: `false`)
|
||||||
|
|
||||||
### `LeastUsedChannelOptimizer`
|
### `LeastUsedChannelOptimizer`
|
||||||
This algorithm assigns the channel of the OWF APs based on the following logic:
|
This algorithm assigns the channel of the OWF APs based on the following logic:
|
||||||
@@ -59,6 +61,8 @@ the value. This is only for testing and re-initialization.
|
|||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* `mode`: "random"
|
* `mode`: "random"
|
||||||
|
* `setDifferentTxPowerPerAp`: If true, will set a different random tx power per AP. If false, it will set the same random tx power for all APs.
|
||||||
|
* values: `true`, `false` (default: `false`)
|
||||||
|
|
||||||
### `MeasurementBasedApClientTPC`
|
### `MeasurementBasedApClientTPC`
|
||||||
This algorithm tries to assign the Tx power of the OWF APs based on the
|
This algorithm tries to assign the Tx power of the OWF APs based on the
|
||||||
@@ -72,6 +76,8 @@ each AP):
|
|||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* `mode`: "measure_ap_client"
|
* `mode`: "measure_ap_client"
|
||||||
|
* `targetMcs`: The target MCS index
|
||||||
|
* values: 0-9 (default: 8)
|
||||||
|
|
||||||
### `MeasurementBasedApApTPC`
|
### `MeasurementBasedApApTPC`
|
||||||
This algorithm tries to assign the Tx power of the OWF APs by getting a set of
|
This algorithm tries to assign the Tx power of the OWF APs by getting a set of
|
||||||
@@ -87,3 +93,7 @@ levels of these APs will be determined by the following steps:
|
|||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* `mode`: "measure_ap_ap"
|
* `mode`: "measure_ap_ap"
|
||||||
|
* `coverageThreshold`: Coverage threshold between APs in dBm
|
||||||
|
* values: int < 30 (default: -70)
|
||||||
|
* `nthSmallestRssi`: the nth smallest RSSI that is used for tx power calculation
|
||||||
|
* values: int >= 0 (default: 0)
|
||||||
|
|||||||
@@ -186,24 +186,27 @@ public class RRMAlgorithm {
|
|||||||
logger.info("Using default algorithm mode...");
|
logger.info("Using default algorithm mode...");
|
||||||
// fall through
|
// fall through
|
||||||
case UnmanagedApAwareChannelOptimizer.ALGORITHM_ID:
|
case UnmanagedApAwareChannelOptimizer.ALGORITHM_ID:
|
||||||
optimizer = new UnmanagedApAwareChannelOptimizer(
|
optimizer = UnmanagedApAwareChannelOptimizer.makeWithArgs(
|
||||||
modeler.getDataModelCopy(),
|
modeler.getDataModelCopy(),
|
||||||
zone,
|
zone,
|
||||||
deviceDataManager
|
deviceDataManager,
|
||||||
|
args
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case RandomChannelInitializer.ALGORITHM_ID:
|
case RandomChannelInitializer.ALGORITHM_ID:
|
||||||
optimizer = new RandomChannelInitializer(
|
optimizer = RandomChannelInitializer.makeWithArgs(
|
||||||
modeler.getDataModelCopy(),
|
modeler.getDataModelCopy(),
|
||||||
zone,
|
zone,
|
||||||
deviceDataManager
|
deviceDataManager,
|
||||||
|
args
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case LeastUsedChannelOptimizer.ALGORITHM_ID:
|
case LeastUsedChannelOptimizer.ALGORITHM_ID:
|
||||||
optimizer = new LeastUsedChannelOptimizer(
|
optimizer = LeastUsedChannelOptimizer.makeWithArgs(
|
||||||
modeler.getDataModelCopy(),
|
modeler.getDataModelCopy(),
|
||||||
zone,
|
zone,
|
||||||
deviceDataManager
|
deviceDataManager,
|
||||||
|
args
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -233,31 +236,35 @@ public class RRMAlgorithm {
|
|||||||
logger.info("Using default algorithm mode...");
|
logger.info("Using default algorithm mode...");
|
||||||
// fall through
|
// fall through
|
||||||
case MeasurementBasedApApTPC.ALGORITHM_ID:
|
case MeasurementBasedApApTPC.ALGORITHM_ID:
|
||||||
optimizer = new MeasurementBasedApApTPC(
|
optimizer = MeasurementBasedApApTPC.makeWithArgs(
|
||||||
modeler.getDataModelCopy(),
|
modeler.getDataModelCopy(),
|
||||||
zone,
|
zone,
|
||||||
deviceDataManager
|
deviceDataManager,
|
||||||
|
args
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case RandomTxPowerInitializer.ALGORITHM_ID:
|
case RandomTxPowerInitializer.ALGORITHM_ID:
|
||||||
optimizer = new RandomTxPowerInitializer(
|
optimizer = RandomTxPowerInitializer.makeWithArgs(
|
||||||
modeler.getDataModelCopy(),
|
modeler.getDataModelCopy(),
|
||||||
zone,
|
zone,
|
||||||
deviceDataManager
|
deviceDataManager,
|
||||||
|
args
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case MeasurementBasedApClientTPC.ALGORITHM_ID:
|
case MeasurementBasedApClientTPC.ALGORITHM_ID:
|
||||||
optimizer = new MeasurementBasedApClientTPC(
|
optimizer = MeasurementBasedApClientTPC.makeWithArgs(
|
||||||
modeler.getDataModelCopy(),
|
modeler.getDataModelCopy(),
|
||||||
zone,
|
zone,
|
||||||
deviceDataManager
|
deviceDataManager,
|
||||||
|
args
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case LocationBasedOptimalTPC.ALGORITHM_ID:
|
case LocationBasedOptimalTPC.ALGORITHM_ID:
|
||||||
optimizer = new LocationBasedOptimalTPC(
|
optimizer = LocationBasedOptimalTPC.makeWithArgs(
|
||||||
modeler.getDataModelCopy(),
|
modeler.getDataModelCopy(),
|
||||||
zone,
|
zone,
|
||||||
deviceDataManager
|
deviceDataManager,
|
||||||
|
args
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,16 @@ public class LeastUsedChannelOptimizer extends ChannelOptimizer {
|
|||||||
/** The PRNG instance. */
|
/** The PRNG instance. */
|
||||||
protected final Random rng = new Random();
|
protected final Random rng = new Random();
|
||||||
|
|
||||||
|
/** Factory method to parse generic args map into the proper constructor */
|
||||||
|
public static LeastUsedChannelOptimizer makeWithArgs(
|
||||||
|
DataModel model,
|
||||||
|
String zone,
|
||||||
|
DeviceDataManager deviceDataManager,
|
||||||
|
Map<String, String> args
|
||||||
|
) {
|
||||||
|
return new LeastUsedChannelOptimizer(model, zone, deviceDataManager);
|
||||||
|
}
|
||||||
|
|
||||||
/** Constructor. */
|
/** Constructor. */
|
||||||
public LeastUsedChannelOptimizer(
|
public LeastUsedChannelOptimizer(
|
||||||
DataModel model,
|
DataModel model,
|
||||||
|
|||||||
@@ -38,11 +38,36 @@ public class RandomChannelInitializer extends ChannelOptimizer {
|
|||||||
/** The RRM algorithm ID. */
|
/** The RRM algorithm ID. */
|
||||||
public static final String ALGORITHM_ID = "random";
|
public static final String ALGORITHM_ID = "random";
|
||||||
|
|
||||||
|
/** Default value for setDifferentChannelPerAp */
|
||||||
|
public static final boolean DEFAULT_SET_DIFFERENT_CHANNEL_PER_AP = false;
|
||||||
|
|
||||||
/** The PRNG instance. */
|
/** The PRNG instance. */
|
||||||
private final Random rng;
|
private final Random rng;
|
||||||
|
|
||||||
/** Whether to set a different value per AP or use a single value for all APs */
|
/** Whether to set a different value per AP or use a single value for all APs */
|
||||||
private final boolean setDifferentChannelsPerAp;
|
private final boolean setDifferentChannelPerAp;
|
||||||
|
|
||||||
|
/** Factory method to parse generic args map into the proper constructor */
|
||||||
|
public static RandomChannelInitializer makeWithArgs(
|
||||||
|
DataModel model,
|
||||||
|
String zone,
|
||||||
|
DeviceDataManager deviceDataManager,
|
||||||
|
Map<String, String> args
|
||||||
|
) {
|
||||||
|
boolean setDifferentChannelPerAp = DEFAULT_SET_DIFFERENT_CHANNEL_PER_AP;
|
||||||
|
|
||||||
|
String arg;
|
||||||
|
if ((arg = args.get("setDifferentChannelPerAp")) != null) {
|
||||||
|
setDifferentChannelPerAp = Boolean.parseBoolean(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RandomChannelInitializer(
|
||||||
|
model,
|
||||||
|
zone,
|
||||||
|
deviceDataManager,
|
||||||
|
setDifferentChannelPerAp
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Constructor. */
|
/** Constructor. */
|
||||||
public RandomChannelInitializer(
|
public RandomChannelInitializer(
|
||||||
@@ -50,7 +75,12 @@ public class RandomChannelInitializer extends ChannelOptimizer {
|
|||||||
String zone,
|
String zone,
|
||||||
DeviceDataManager deviceDataManager
|
DeviceDataManager deviceDataManager
|
||||||
) {
|
) {
|
||||||
this(model, zone, deviceDataManager, false);
|
this(
|
||||||
|
model,
|
||||||
|
zone,
|
||||||
|
deviceDataManager,
|
||||||
|
DEFAULT_SET_DIFFERENT_CHANNEL_PER_AP
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Constructor (allows setting different channel per AP) */
|
/** Constructor (allows setting different channel per AP) */
|
||||||
@@ -58,13 +88,13 @@ public class RandomChannelInitializer extends ChannelOptimizer {
|
|||||||
DataModel model,
|
DataModel model,
|
||||||
String zone,
|
String zone,
|
||||||
DeviceDataManager deviceDataManager,
|
DeviceDataManager deviceDataManager,
|
||||||
boolean setDifferentChannelsPerAp
|
boolean setDifferentChannelPerAp
|
||||||
) {
|
) {
|
||||||
this(
|
this(
|
||||||
model,
|
model,
|
||||||
zone,
|
zone,
|
||||||
deviceDataManager,
|
deviceDataManager,
|
||||||
setDifferentChannelsPerAp,
|
setDifferentChannelPerAp,
|
||||||
new Random()
|
new Random()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -77,11 +107,11 @@ public class RandomChannelInitializer extends ChannelOptimizer {
|
|||||||
DataModel model,
|
DataModel model,
|
||||||
String zone,
|
String zone,
|
||||||
DeviceDataManager deviceDataManager,
|
DeviceDataManager deviceDataManager,
|
||||||
boolean setDifferentChannelsPerAp,
|
boolean setDifferentChannelPerAp,
|
||||||
Random rng
|
Random rng
|
||||||
) {
|
) {
|
||||||
super(model, zone, deviceDataManager);
|
super(model, zone, deviceDataManager);
|
||||||
this.setDifferentChannelsPerAp = setDifferentChannelsPerAp;
|
this.setDifferentChannelPerAp = setDifferentChannelPerAp;
|
||||||
this.rng = rng;
|
this.rng = rng;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,13 +173,13 @@ public class RandomChannelInitializer extends ChannelOptimizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Randomly assign all the devices to the same channel if
|
// Randomly assign all the devices to the same channel if
|
||||||
// setDifferentChannelsPerAp is false otherwise, assigns
|
// setDifferentChannelPerAp is false otherwise, assigns
|
||||||
// each device to a random channel
|
// each device to a random channel
|
||||||
int defaultChannelIndex = rng.nextInt(availableChannelsList.size());
|
int defaultChannelIndex = rng.nextInt(availableChannelsList.size());
|
||||||
|
|
||||||
for (String serialNumber : entry.getValue()) {
|
for (String serialNumber : entry.getValue()) {
|
||||||
int newChannel = availableChannelsList.get(
|
int newChannel = availableChannelsList.get(
|
||||||
this.setDifferentChannelsPerAp
|
this.setDifferentChannelPerAp
|
||||||
? rng.nextInt(availableChannelsList.size()) : defaultChannelIndex
|
? rng.nextInt(availableChannelsList.size()) : defaultChannelIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,20 @@ public class UnmanagedApAwareChannelOptimizer
|
|||||||
/** The default weight for nonOWF APs. */
|
/** The default weight for nonOWF APs. */
|
||||||
private static final int DEFAULT_WEIGHT = 2;
|
private static final int DEFAULT_WEIGHT = 2;
|
||||||
|
|
||||||
|
/** Factory method to parse generic args map into the proper constructor */
|
||||||
|
public static UnmanagedApAwareChannelOptimizer makeWithArgs(
|
||||||
|
DataModel model,
|
||||||
|
String zone,
|
||||||
|
DeviceDataManager deviceDataManager,
|
||||||
|
Map<String, String> args
|
||||||
|
) {
|
||||||
|
return new UnmanagedApAwareChannelOptimizer(
|
||||||
|
model,
|
||||||
|
zone,
|
||||||
|
deviceDataManager
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Constructor. */
|
/** Constructor. */
|
||||||
public UnmanagedApAwareChannelOptimizer(
|
public UnmanagedApAwareChannelOptimizer(
|
||||||
DataModel model,
|
DataModel model,
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ public class LocationBasedOptimalTPC extends TPC {
|
|||||||
/** The RRM algorithm ID. */
|
/** The RRM algorithm ID. */
|
||||||
public static final String ALGORITHM_ID = "location_optimal";
|
public static final String ALGORITHM_ID = "location_optimal";
|
||||||
|
|
||||||
|
/** Factory method to parse generic args map into the proper constructor */
|
||||||
|
public static LocationBasedOptimalTPC makeWithArgs(
|
||||||
|
DataModel model,
|
||||||
|
String zone,
|
||||||
|
DeviceDataManager deviceDataManager,
|
||||||
|
Map<String, String> args
|
||||||
|
) {
|
||||||
|
return new LocationBasedOptimalTPC(model, zone, deviceDataManager);
|
||||||
|
}
|
||||||
|
|
||||||
/** Constructor. */
|
/** Constructor. */
|
||||||
public LocationBasedOptimalTPC(
|
public LocationBasedOptimalTPC(
|
||||||
DataModel model,
|
DataModel model,
|
||||||
|
|||||||
@@ -64,6 +64,62 @@ public class MeasurementBasedApApTPC extends TPC {
|
|||||||
/** Nth smallest RSSI (zero-indexed) is used for Tx power calculation */
|
/** Nth smallest RSSI (zero-indexed) is used for Tx power calculation */
|
||||||
private final int nthSmallestRssi; // TODO non-zero values untested
|
private final int nthSmallestRssi; // TODO non-zero values untested
|
||||||
|
|
||||||
|
/** Factory method to parse generic args map into the proper constructor */
|
||||||
|
public static MeasurementBasedApApTPC makeWithArgs(
|
||||||
|
DataModel model,
|
||||||
|
String zone,
|
||||||
|
DeviceDataManager deviceDataManager,
|
||||||
|
Map<String, String> args
|
||||||
|
) {
|
||||||
|
int coverageThreshold = DEFAULT_COVERAGE_THRESHOLD;
|
||||||
|
int nthSmallestRssi = DEFAULT_NTH_SMALLEST_RSSI;
|
||||||
|
|
||||||
|
String arg;
|
||||||
|
if ((arg = args.get("coverageThreshold")) != null) {
|
||||||
|
try {
|
||||||
|
int parsedCoverageThreshold = Integer.parseInt(arg);
|
||||||
|
if (parsedCoverageThreshold > 30) {
|
||||||
|
logger.error(
|
||||||
|
"Invalid value passed for coverageThreshold - must be less than 30. Using default value."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
coverageThreshold = parsedCoverageThreshold;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
logger.error(
|
||||||
|
"Invalid integer passed to parameter coverageThreshold, using default value",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((arg = args.get("nthSmallestRssi")) != null) {
|
||||||
|
try {
|
||||||
|
int parsedNthSmallestRssi = Integer.parseInt(arg);
|
||||||
|
if (parsedNthSmallestRssi < 0) {
|
||||||
|
logger.error(
|
||||||
|
"Invalid value passed for nthSmallestRssi - must be greater than 0. Using default value."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
nthSmallestRssi = parsedNthSmallestRssi;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
logger.error(
|
||||||
|
"Invalid integer passed to parameter nthSmallestRssi, using default value",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MeasurementBasedApApTPC(
|
||||||
|
model,
|
||||||
|
zone,
|
||||||
|
deviceDataManager,
|
||||||
|
coverageThreshold,
|
||||||
|
nthSmallestRssi
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Constructor. */
|
/** Constructor. */
|
||||||
public MeasurementBasedApApTPC(
|
public MeasurementBasedApApTPC(
|
||||||
DataModel model,
|
DataModel model,
|
||||||
|
|||||||
@@ -61,6 +61,43 @@ public class MeasurementBasedApClientTPC extends TPC {
|
|||||||
/** The target MCS index. */
|
/** The target MCS index. */
|
||||||
private final int targetMcs;
|
private final int targetMcs;
|
||||||
|
|
||||||
|
/** Factory method to parse generic args map into the proper constructor */
|
||||||
|
public static MeasurementBasedApClientTPC makeWithArgs(
|
||||||
|
DataModel model,
|
||||||
|
String zone,
|
||||||
|
DeviceDataManager deviceDataManager,
|
||||||
|
Map<String, String> args
|
||||||
|
) {
|
||||||
|
int targetMcs = DEFAULT_TARGET_MCS;
|
||||||
|
|
||||||
|
String arg;
|
||||||
|
if ((arg = args.get("targetMcs")) != null) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
int parsedTargetMcs = Integer.parseInt(arg);
|
||||||
|
if (targetMcs < 0) {
|
||||||
|
logger.error(
|
||||||
|
"Invalid value passed for targetMcs - must be greater than 0. Using default value."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
targetMcs = parsedTargetMcs;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
logger.error(
|
||||||
|
"Invalid integer passed to parameter targetMcs, using default value",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MeasurementBasedApClientTPC(
|
||||||
|
model,
|
||||||
|
zone,
|
||||||
|
deviceDataManager,
|
||||||
|
targetMcs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Constructor (uses default target MCS index). */
|
/** Constructor (uses default target MCS index). */
|
||||||
public MeasurementBasedApClientTPC(
|
public MeasurementBasedApClientTPC(
|
||||||
DataModel model,
|
DataModel model,
|
||||||
|
|||||||
@@ -31,19 +31,54 @@ public class RandomTxPowerInitializer extends TPC {
|
|||||||
/** The RRM algorithm ID. */
|
/** The RRM algorithm ID. */
|
||||||
public static final String ALGORITHM_ID = "random";
|
public static final String ALGORITHM_ID = "random";
|
||||||
|
|
||||||
|
/** Default value for setDifferentTxPowerPerAp */
|
||||||
|
public static final boolean DEFAULT_SET_DIFFERENT_TX_POWER_PER_AP =
|
||||||
|
false;
|
||||||
|
|
||||||
/** The PRNG instance. */
|
/** The PRNG instance. */
|
||||||
private final Random rng;
|
private final Random rng;
|
||||||
|
|
||||||
/** Whether to set a different value per AP or use a single value for all APs */
|
/** Whether to set a different value per AP or use a single value for all APs */
|
||||||
private final boolean setDifferentTxPowerPerAp;
|
private final boolean setDifferentTxPowerPerAp;
|
||||||
|
|
||||||
/** Constructor (uses random tx power). */
|
/** Factory method to parse generic args map into the proper constructor */
|
||||||
|
public static RandomTxPowerInitializer makeWithArgs(
|
||||||
|
DataModel model,
|
||||||
|
String zone,
|
||||||
|
DeviceDataManager deviceDataManager,
|
||||||
|
Map<String, String> args
|
||||||
|
) {
|
||||||
|
boolean setDifferentTxPowerPerAp =
|
||||||
|
DEFAULT_SET_DIFFERENT_TX_POWER_PER_AP;
|
||||||
|
|
||||||
|
String arg;
|
||||||
|
if ((arg = args.get("setDifferentTxPowerPerAp")) != null) {
|
||||||
|
setDifferentTxPowerPerAp = Boolean.parseBoolean(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RandomTxPowerInitializer(
|
||||||
|
model,
|
||||||
|
zone,
|
||||||
|
deviceDataManager,
|
||||||
|
setDifferentTxPowerPerAp
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor (uses random tx power per AP and allows passing in a custom
|
||||||
|
* Random class to allow seeding).
|
||||||
|
*/
|
||||||
public RandomTxPowerInitializer(
|
public RandomTxPowerInitializer(
|
||||||
DataModel model,
|
DataModel model,
|
||||||
String zone,
|
String zone,
|
||||||
DeviceDataManager deviceDataManager
|
DeviceDataManager deviceDataManager
|
||||||
) {
|
) {
|
||||||
this(model, zone, deviceDataManager, false);
|
this(
|
||||||
|
model,
|
||||||
|
zone,
|
||||||
|
deviceDataManager,
|
||||||
|
DEFAULT_SET_DIFFERENT_TX_POWER_PER_AP
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Constructor (uses random tx power per AP). */
|
/** Constructor (uses random tx power per AP). */
|
||||||
|
|||||||
Reference in New Issue
Block a user