init
This commit is contained in:
		@@ -0,0 +1,37 @@
 | 
			
		||||
# Sentinel DataSource Etcd
 | 
			
		||||
 | 
			
		||||
Sentinel DataSource Etcd provides integration with etcd so that etcd
 | 
			
		||||
can be the dynamic rule data source of Sentinel. The data source uses push model (watcher).
 | 
			
		||||
 | 
			
		||||
To use Sentinel DataSource Etcd, you should add the following dependency:
 | 
			
		||||
 | 
			
		||||
```xml
 | 
			
		||||
<dependency>
 | 
			
		||||
    <groupId>com.alibaba.csp</groupId>
 | 
			
		||||
    <artifactId>sentinel-datasource-etcd</artifactId>
 | 
			
		||||
    <version>x.y.z</version>
 | 
			
		||||
</dependency>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
We could configure Etcd connection configuration by config file (for example `sentinel.properties`):
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
csp.sentinel.etcd.endpoints=http://ip1:port1,http://ip2:port2
 | 
			
		||||
csp.sentinel.etcd.user=your_user
 | 
			
		||||
csp.sentinel.etcd.password=your_password
 | 
			
		||||
csp.sentinel.etcd.charset=your_charset
 | 
			
		||||
csp.sentinel.etcd.auth.enable=true # if ture, then open user/password or ssl check
 | 
			
		||||
csp.sentinel.etcd.authority=authority # ssl
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Or we could configure via JVM -D args or via `SentinelConfig.setConfig(key, value)`.
 | 
			
		||||
 | 
			
		||||
Then we can create an `EtcdDataSource` and register to rule managers. For instance:
 | 
			
		||||
 | 
			
		||||
```java
 | 
			
		||||
// `rule_key` is the rule config key
 | 
			
		||||
ReadableDataSource<String, List<FlowRule>> flowRuleEtcdDataSource = new EtcdDataSource<>(rule_key, (rule) -> JSON.parseArray(rule, FlowRule.class));
 | 
			
		||||
FlowRuleManager.register2Property(flowRuleEtcdDataSource.getProperty());
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
We've also provided an example: [sentinel-demo-etcd-datasource](https://github.com/alibaba/Sentinel/tree/master/sentinel-demo/sentinel-demo-etcd-datasource)
 | 
			
		||||
							
								
								
									
										63
									
								
								sentinel/sentinel-extension/sentinel-datasource-etcd/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								sentinel/sentinel-extension/sentinel-datasource-etcd/pom.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
 | 
			
		||||
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
			
		||||
    <parent>
 | 
			
		||||
        <artifactId>sentinel-extension</artifactId>
 | 
			
		||||
        <groupId>com.alibaba.csp</groupId>
 | 
			
		||||
        <version>1.8.3</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
    <modelVersion>4.0.0</modelVersion>
 | 
			
		||||
 | 
			
		||||
    <artifactId>sentinel-datasource-etcd</artifactId>
 | 
			
		||||
    <packaging>jar</packaging>
 | 
			
		||||
 | 
			
		||||
    <properties>
 | 
			
		||||
        <maven.compiler.source>1.8</maven.compiler.source>
 | 
			
		||||
        <maven.compiler.target>1.8</maven.compiler.target>
 | 
			
		||||
        <jetcd.version>0.3.0</jetcd.version>
 | 
			
		||||
    </properties>
 | 
			
		||||
 | 
			
		||||
    <dependencies>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.alibaba.csp</groupId>
 | 
			
		||||
            <artifactId>sentinel-datasource-extension</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>io.etcd</groupId>
 | 
			
		||||
            <artifactId>jetcd-core</artifactId>
 | 
			
		||||
            <version>${jetcd.version}</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>junit</groupId>
 | 
			
		||||
            <artifactId>junit</artifactId>
 | 
			
		||||
            <scope>test</scope>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.alibaba</groupId>
 | 
			
		||||
            <artifactId>fastjson</artifactId>
 | 
			
		||||
            <scope>test</scope>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
    </dependencies>
 | 
			
		||||
 | 
			
		||||
    <build>
 | 
			
		||||
        <plugins>
 | 
			
		||||
            <plugin>
 | 
			
		||||
                <groupId>org.apache.maven.plugins</groupId>
 | 
			
		||||
                <artifactId>maven-compiler-plugin</artifactId>
 | 
			
		||||
                <configuration>
 | 
			
		||||
                    <source>${maven.compiler.source}</source>
 | 
			
		||||
                    <target>${maven.compiler.target}</target>
 | 
			
		||||
                </configuration>
 | 
			
		||||
            </plugin>
 | 
			
		||||
        </plugins>
 | 
			
		||||
    </build>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</project>
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed 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.
 | 
			
		||||
 */
 | 
			
		||||
package com.alibaba.csp.sentinel.datasource.etcd;
 | 
			
		||||
 | 
			
		||||
import com.alibaba.csp.sentinel.config.SentinelConfig;
 | 
			
		||||
import com.alibaba.csp.sentinel.util.StringUtil;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Etcd connection configuration.
 | 
			
		||||
 *
 | 
			
		||||
 * @author lianglin
 | 
			
		||||
 * @since 1.7.0
 | 
			
		||||
 */
 | 
			
		||||
public final class EtcdConfig {
 | 
			
		||||
 | 
			
		||||
    public final static String END_POINTS = "csp.sentinel.etcd.endpoints";
 | 
			
		||||
    public final static String USER = "csp.sentinel.etcd.user";
 | 
			
		||||
    public final static String PASSWORD = "csp.sentinel.etcd.password";
 | 
			
		||||
    public final static String CHARSET = "csp.sentinel.etcd.charset";
 | 
			
		||||
    public final static String AUTH_ENABLE = "csp.sentinel.etcd.auth.enable";
 | 
			
		||||
    public final static String AUTHORITY = "csp.sentinel.etcd.authority";
 | 
			
		||||
 | 
			
		||||
    private final static String ENABLED = "true";
 | 
			
		||||
 | 
			
		||||
    public static String getEndPoints() {
 | 
			
		||||
        return SentinelConfig.getConfig(END_POINTS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static String getUser() {
 | 
			
		||||
        return SentinelConfig.getConfig(USER);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static String getPassword() {
 | 
			
		||||
        return SentinelConfig.getConfig(PASSWORD);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static String getCharset() {
 | 
			
		||||
        String etcdCharset = SentinelConfig.getConfig(CHARSET);
 | 
			
		||||
        if (StringUtil.isNotBlank(etcdCharset)) {
 | 
			
		||||
            return etcdCharset;
 | 
			
		||||
        }
 | 
			
		||||
        return SentinelConfig.charset();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static boolean isAuthEnable() {
 | 
			
		||||
        return ENABLED.equalsIgnoreCase(SentinelConfig.getConfig(AUTH_ENABLE));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static String getAuthority() {
 | 
			
		||||
        return SentinelConfig.getConfig(AUTHORITY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private EtcdConfig() {}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,124 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed 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.
 | 
			
		||||
 */
 | 
			
		||||
package com.alibaba.csp.sentinel.datasource.etcd;
 | 
			
		||||
 | 
			
		||||
import com.alibaba.csp.sentinel.datasource.AbstractDataSource;
 | 
			
		||||
import com.alibaba.csp.sentinel.datasource.Converter;
 | 
			
		||||
import com.alibaba.csp.sentinel.log.RecordLog;
 | 
			
		||||
 | 
			
		||||
import io.etcd.jetcd.ByteSequence;
 | 
			
		||||
import io.etcd.jetcd.Client;
 | 
			
		||||
import io.etcd.jetcd.KeyValue;
 | 
			
		||||
import io.etcd.jetcd.Watch;
 | 
			
		||||
import io.etcd.jetcd.kv.GetResponse;
 | 
			
		||||
import io.etcd.jetcd.watch.WatchEvent;
 | 
			
		||||
 | 
			
		||||
import java.nio.charset.Charset;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A read-only {@code DataSource} with Etcd backend. When the data in Etcd backend has been modified,
 | 
			
		||||
 * Etcd will automatically push the new value so that the dynamic configuration can be real-time.
 | 
			
		||||
 *
 | 
			
		||||
 * @author lianglin
 | 
			
		||||
 * @since 1.7.0
 | 
			
		||||
 */
 | 
			
		||||
public class EtcdDataSource<T> extends AbstractDataSource<String, T> {
 | 
			
		||||
 | 
			
		||||
    private final Client client;
 | 
			
		||||
    private Watch.Watcher watcher;
 | 
			
		||||
 | 
			
		||||
    private final String key;
 | 
			
		||||
    private Charset charset = Charset.forName(EtcdConfig.getCharset());
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create an etcd data-source. The connection configuration will be retrieved from {@link EtcdConfig}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key    config key
 | 
			
		||||
     * @param parser data parser
 | 
			
		||||
     */
 | 
			
		||||
    public EtcdDataSource(String key, Converter<String, T> parser) {
 | 
			
		||||
        super(parser);
 | 
			
		||||
        if (!EtcdConfig.isAuthEnable()) {
 | 
			
		||||
            this.client = Client.builder()
 | 
			
		||||
                .endpoints(EtcdConfig.getEndPoints().split(",")).build();
 | 
			
		||||
        } else {
 | 
			
		||||
            this.client = Client.builder()
 | 
			
		||||
                .endpoints(EtcdConfig.getEndPoints().split(","))
 | 
			
		||||
                .user(ByteSequence.from(EtcdConfig.getUser(), charset))
 | 
			
		||||
                .password(ByteSequence.from(EtcdConfig.getPassword(), charset))
 | 
			
		||||
                .authority(EtcdConfig.getAuthority())
 | 
			
		||||
                .build();
 | 
			
		||||
        }
 | 
			
		||||
        this.key = key;
 | 
			
		||||
        loadInitialConfig();
 | 
			
		||||
        initWatcher();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void loadInitialConfig() {
 | 
			
		||||
        try {
 | 
			
		||||
            T newValue = loadConfig();
 | 
			
		||||
            if (newValue == null) {
 | 
			
		||||
                RecordLog.warn(
 | 
			
		||||
                    "[EtcdDataSource] Initial configuration is null, you may have to check your data source");
 | 
			
		||||
            }
 | 
			
		||||
            getProperty().updateValue(newValue);
 | 
			
		||||
        } catch (Exception ex) {
 | 
			
		||||
            RecordLog.warn("[EtcdDataSource] Error when loading initial configuration", ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void initWatcher() {
 | 
			
		||||
        watcher = client.getWatchClient().watch(ByteSequence.from(key, charset), (watchResponse) -> {
 | 
			
		||||
            for (WatchEvent watchEvent : watchResponse.getEvents()) {
 | 
			
		||||
                WatchEvent.EventType eventType = watchEvent.getEventType();
 | 
			
		||||
                if (eventType == WatchEvent.EventType.PUT) {
 | 
			
		||||
                    try {
 | 
			
		||||
                        T newValue = loadConfig();
 | 
			
		||||
                        getProperty().updateValue(newValue);
 | 
			
		||||
                    } catch (Exception e) {
 | 
			
		||||
                        RecordLog.warn("[EtcdDataSource] Failed to update config", e);
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (eventType == WatchEvent.EventType.DELETE) {
 | 
			
		||||
                    RecordLog.info("[EtcdDataSource] Cleaning config for key <{}>", key);
 | 
			
		||||
                    getProperty().updateValue(null);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String readSource() throws Exception {
 | 
			
		||||
        CompletableFuture<GetResponse> responseFuture = client.getKVClient().get(ByteSequence.from(key, charset));
 | 
			
		||||
        List<KeyValue> kvs = responseFuture.get().getKvs();
 | 
			
		||||
        return kvs.size() == 0 ? null : kvs.get(0).getValue().toString(charset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void close() {
 | 
			
		||||
        if (watcher != null) {
 | 
			
		||||
            try {
 | 
			
		||||
                watcher.close();
 | 
			
		||||
            } catch (Exception ex) {
 | 
			
		||||
                RecordLog.info("[EtcdDataSource] Failed to close watcher", ex);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (client != null) {
 | 
			
		||||
            client.close();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,123 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed 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.
 | 
			
		||||
 */
 | 
			
		||||
package com.alibaba.csp.sentinel.datasource.etcd;
 | 
			
		||||
 | 
			
		||||
import com.alibaba.csp.sentinel.config.SentinelConfig;
 | 
			
		||||
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
 | 
			
		||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
 | 
			
		||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
 | 
			
		||||
import com.alibaba.fastjson.JSON;
 | 
			
		||||
import io.etcd.jetcd.ByteSequence;
 | 
			
		||||
import io.etcd.jetcd.Client;
 | 
			
		||||
import io.etcd.jetcd.KV;
 | 
			
		||||
import org.junit.After;
 | 
			
		||||
import org.junit.Assert;
 | 
			
		||||
import org.junit.Before;
 | 
			
		||||
import org.junit.Ignore;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lianglin
 | 
			
		||||
 * @since 1.7.0
 | 
			
		||||
 */
 | 
			
		||||
@Ignore(value = "Before run this test, you need to set up your etcd server.")
 | 
			
		||||
public class EtcdDataSourceTest {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private final String endPoints = "http://127.0.0.1:2379";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    public void setUp() {
 | 
			
		||||
        SentinelConfig.setConfig(EtcdConfig.END_POINTS, endPoints);
 | 
			
		||||
        FlowRuleManager.loadRules(new ArrayList<>());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @After
 | 
			
		||||
    public void tearDown() {
 | 
			
		||||
        SentinelConfig.setConfig(EtcdConfig.END_POINTS, "");
 | 
			
		||||
        FlowRuleManager.loadRules(new ArrayList<>());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testReadSource() throws Exception {
 | 
			
		||||
        EtcdDataSource dataSource = new EtcdDataSource("foo", value -> value);
 | 
			
		||||
        KV kvClient = Client.builder()
 | 
			
		||||
                .endpoints(endPoints)
 | 
			
		||||
                .build().getKVClient();
 | 
			
		||||
 | 
			
		||||
        kvClient.put(ByteSequence.from("foo".getBytes()), ByteSequence.from("test".getBytes()));
 | 
			
		||||
        Assert.assertNotNull(dataSource.readSource().equals("test"));
 | 
			
		||||
 | 
			
		||||
        kvClient.put(ByteSequence.from("foo".getBytes()), ByteSequence.from("test2".getBytes()));
 | 
			
		||||
        Assert.assertNotNull(dataSource.getProperty().equals("test2"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testDynamicUpdate() throws InterruptedException {
 | 
			
		||||
        String demo_key = "etcd_demo_key";
 | 
			
		||||
        ReadableDataSource<String, List<FlowRule>> flowRuleEtcdDataSource = new EtcdDataSource<>(demo_key, (value) -> JSON.parseArray(value, FlowRule.class));
 | 
			
		||||
        FlowRuleManager.register2Property(flowRuleEtcdDataSource.getProperty());
 | 
			
		||||
 | 
			
		||||
        KV kvClient = Client.builder()
 | 
			
		||||
                .endpoints(endPoints)
 | 
			
		||||
                .build().getKVClient();
 | 
			
		||||
 | 
			
		||||
        final String rule1 = "[\n"
 | 
			
		||||
                + "  {\n"
 | 
			
		||||
                + "    \"resource\": \"TestResource\",\n"
 | 
			
		||||
                + "    \"controlBehavior\": 0,\n"
 | 
			
		||||
                + "    \"count\": 5.0,\n"
 | 
			
		||||
                + "    \"grade\": 1,\n"
 | 
			
		||||
                + "    \"limitApp\": \"default\",\n"
 | 
			
		||||
                + "    \"strategy\": 0\n"
 | 
			
		||||
                + "  }\n"
 | 
			
		||||
                + "]";
 | 
			
		||||
 | 
			
		||||
        kvClient.put(ByteSequence.from(demo_key.getBytes()), ByteSequence.from(rule1.getBytes()));
 | 
			
		||||
        Thread.sleep(1000);
 | 
			
		||||
 | 
			
		||||
        FlowRule flowRule = FlowRuleManager.getRules().get(0);
 | 
			
		||||
        Assert.assertTrue(flowRule.getResource().equals("TestResource"));
 | 
			
		||||
        Assert.assertTrue(flowRule.getCount() == 5.0);
 | 
			
		||||
        Assert.assertTrue(flowRule.getGrade() == 1);
 | 
			
		||||
 | 
			
		||||
        final String rule2 = "[\n"
 | 
			
		||||
                + "  {\n"
 | 
			
		||||
                + "    \"resource\": \"TestResource\",\n"
 | 
			
		||||
                + "    \"controlBehavior\": 0,\n"
 | 
			
		||||
                + "    \"count\": 6.0,\n"
 | 
			
		||||
                + "    \"grade\": 3,\n"
 | 
			
		||||
                + "    \"limitApp\": \"default\",\n"
 | 
			
		||||
                + "    \"strategy\": 0\n"
 | 
			
		||||
                + "  }\n"
 | 
			
		||||
                + "]";
 | 
			
		||||
 | 
			
		||||
        kvClient.put(ByteSequence.from(demo_key.getBytes()), ByteSequence.from(rule2.getBytes()));
 | 
			
		||||
        Thread.sleep(1000);
 | 
			
		||||
 | 
			
		||||
        flowRule = FlowRuleManager.getRules().get(0);
 | 
			
		||||
        Assert.assertTrue(flowRule.getResource().equals("TestResource"));
 | 
			
		||||
        Assert.assertTrue(flowRule.getCount() == 6.0);
 | 
			
		||||
        Assert.assertTrue(flowRule.getGrade() == 3);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user